Repository: ipfs/js-ipfs
Branch: master
Commit: bf1bc8b18d75
Files: 1310
Total size: 20.1 MB
Directory structure:
gitextract_52p81fdk/
├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── config.yml
│ │ └── open_an_issue.md
│ ├── config.yml
│ ├── dependabot.yml
│ └── workflows/
│ ├── examples.yml
│ ├── externals.yml
│ ├── stale.yml
│ └── test.yml
├── .gitignore
├── .release-please-manifest.json
├── .release-please.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── COPYRIGHT
├── Dockerfile.latest
├── Dockerfile.next
├── LICENSE
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── docs/
│ ├── ARCHITECTURE.md
│ ├── BROWSERS.md
│ ├── CLI.md
│ ├── CONFIG.md
│ ├── CORS.md
│ ├── DAEMON.md
│ ├── DELEGATE_ROUTERS.md
│ ├── DEVELOPMENT.md
│ ├── DOCKER.md
│ ├── EARLY_TESTERS.md
│ ├── FAQ.md
│ ├── IPLD.md
│ ├── MIGRATION-TO-ASYNC-AWAIT.md
│ ├── MODULE.md
│ ├── MONITORING.md
│ ├── README.md
│ ├── RELEASES.md
│ ├── RELEASE_ISSUE_TEMPLATE.md
│ ├── core-api/
│ │ ├── BITSWAP.md
│ │ ├── BLOCK.md
│ │ ├── BOOTSTRAP.md
│ │ ├── CONFIG.md
│ │ ├── DAG.md
│ │ ├── DHT.md
│ │ ├── FILES.md
│ │ ├── KEY.md
│ │ ├── MISCELLANEOUS.md
│ │ ├── NAME.md
│ │ ├── OBJECT.md
│ │ ├── PIN.md
│ │ ├── PUBSUB.md
│ │ ├── README.md
│ │ ├── REFS.md
│ │ ├── REPO.md
│ │ ├── STATS.md
│ │ └── SWARM.md
│ ├── img/
│ │ ├── architecture.monopic
│ │ ├── architecture.txt
│ │ ├── core.monopic
│ │ ├── core.txt
│ │ ├── overview.monopic
│ │ └── overview.txt
│ └── upgrading/
│ ├── v0.62-v0.63.md
│ ├── v0.63-v0.64.md
│ └── v0.64-v0.65.md
├── package-list.json
├── package.json
├── packages/
│ ├── interface-ipfs-core/
│ │ ├── .aegir.js
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── img/
│ │ │ └── badge.sketch
│ │ ├── maintainer.json
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── add-all.js
│ │ │ ├── add.js
│ │ │ ├── bitswap/
│ │ │ │ ├── index.js
│ │ │ │ ├── stat.js
│ │ │ │ ├── transfer.js
│ │ │ │ ├── unwant.js
│ │ │ │ ├── utils.js
│ │ │ │ ├── wantlist-for-peer.js
│ │ │ │ └── wantlist.js
│ │ │ ├── block/
│ │ │ │ ├── get.js
│ │ │ │ ├── index.js
│ │ │ │ ├── put.js
│ │ │ │ ├── rm.js
│ │ │ │ └── stat.js
│ │ │ ├── bootstrap/
│ │ │ │ ├── add.js
│ │ │ │ ├── clear.js
│ │ │ │ ├── index.js
│ │ │ │ ├── list.js
│ │ │ │ ├── reset.js
│ │ │ │ └── rm.js
│ │ │ ├── cat.js
│ │ │ ├── config/
│ │ │ │ ├── get.js
│ │ │ │ ├── index.js
│ │ │ │ ├── profiles/
│ │ │ │ │ ├── apply.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── list.js
│ │ │ │ ├── replace.js
│ │ │ │ └── set.js
│ │ │ ├── dag/
│ │ │ │ ├── export.js
│ │ │ │ ├── get.js
│ │ │ │ ├── import.js
│ │ │ │ ├── index.js
│ │ │ │ ├── put.js
│ │ │ │ ├── resolve.js
│ │ │ │ └── sharness-t0053-dag.js
│ │ │ ├── dht/
│ │ │ │ ├── disabled.js
│ │ │ │ ├── find-peer.js
│ │ │ │ ├── find-provs.js
│ │ │ │ ├── get.js
│ │ │ │ ├── index.js
│ │ │ │ ├── provide.js
│ │ │ │ ├── put.js
│ │ │ │ ├── query.js
│ │ │ │ └── utils.js
│ │ │ ├── files/
│ │ │ │ ├── chmod.js
│ │ │ │ ├── cp.js
│ │ │ │ ├── flush.js
│ │ │ │ ├── index.js
│ │ │ │ ├── ls.js
│ │ │ │ ├── mkdir.js
│ │ │ │ ├── mv.js
│ │ │ │ ├── read.js
│ │ │ │ ├── rm.js
│ │ │ │ ├── stat.js
│ │ │ │ ├── touch.js
│ │ │ │ └── write.js
│ │ │ ├── get.js
│ │ │ ├── index.js
│ │ │ ├── key/
│ │ │ │ ├── gen.js
│ │ │ │ ├── import.js
│ │ │ │ ├── index.js
│ │ │ │ ├── list.js
│ │ │ │ ├── rename.js
│ │ │ │ └── rm.js
│ │ │ ├── ls.js
│ │ │ ├── miscellaneous/
│ │ │ │ ├── dns.js
│ │ │ │ ├── id.js
│ │ │ │ ├── index.js
│ │ │ │ ├── resolve.js
│ │ │ │ ├── stop.js
│ │ │ │ └── version.js
│ │ │ ├── name/
│ │ │ │ ├── index.js
│ │ │ │ ├── publish.js
│ │ │ │ ├── resolve.js
│ │ │ │ └── utils.js
│ │ │ ├── name-pubsub/
│ │ │ │ ├── cancel.js
│ │ │ │ ├── index.js
│ │ │ │ ├── pubsub.js
│ │ │ │ ├── state.js
│ │ │ │ └── subs.js
│ │ │ ├── object/
│ │ │ │ ├── data.js
│ │ │ │ ├── get.js
│ │ │ │ ├── index.js
│ │ │ │ ├── links.js
│ │ │ │ ├── new.js
│ │ │ │ ├── patch/
│ │ │ │ │ ├── add-link.js
│ │ │ │ │ ├── append-data.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── rm-link.js
│ │ │ │ │ └── set-data.js
│ │ │ │ ├── put.js
│ │ │ │ └── stat.js
│ │ │ ├── pin/
│ │ │ │ ├── add-all.js
│ │ │ │ ├── add.js
│ │ │ │ ├── index.js
│ │ │ │ ├── ls.js
│ │ │ │ ├── remote/
│ │ │ │ │ ├── add.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── ls.js
│ │ │ │ │ ├── rm-all.js
│ │ │ │ │ ├── rm.js
│ │ │ │ │ └── service.js
│ │ │ │ ├── rm-all.js
│ │ │ │ ├── rm.js
│ │ │ │ └── utils.js
│ │ │ ├── ping/
│ │ │ │ ├── index.js
│ │ │ │ ├── ping.js
│ │ │ │ └── utils.js
│ │ │ ├── pubsub/
│ │ │ │ ├── index.js
│ │ │ │ ├── ls.js
│ │ │ │ ├── peers.js
│ │ │ │ ├── publish.js
│ │ │ │ ├── subscribe.js
│ │ │ │ ├── unsubscribe.js
│ │ │ │ └── utils.js
│ │ │ ├── refs-local.js
│ │ │ ├── refs.js
│ │ │ ├── repo/
│ │ │ │ ├── gc.js
│ │ │ │ ├── index.js
│ │ │ │ ├── stat.js
│ │ │ │ └── version.js
│ │ │ ├── stats/
│ │ │ │ ├── bitswap.js
│ │ │ │ ├── bw.js
│ │ │ │ ├── index.js
│ │ │ │ ├── repo.js
│ │ │ │ └── utils.js
│ │ │ ├── swarm/
│ │ │ │ ├── addrs.js
│ │ │ │ ├── connect.js
│ │ │ │ ├── disconnect.js
│ │ │ │ ├── index.js
│ │ │ │ ├── local-addrs.js
│ │ │ │ └── peers.js
│ │ │ └── utils/
│ │ │ ├── blockstore-adapter.js
│ │ │ ├── create-sharded-directory.js
│ │ │ ├── create-two-shards.js
│ │ │ ├── dump-shard.js
│ │ │ ├── index.js
│ │ │ ├── ipfs-options-websockets-filter-all.js
│ │ │ ├── is-shard-at-path.js
│ │ │ ├── mocha.js
│ │ │ ├── suite.js
│ │ │ ├── test-timeout.js
│ │ │ ├── traverse-leaf-nodes.js
│ │ │ └── wait-for.js
│ │ ├── test/
│ │ │ ├── fixtures/
│ │ │ │ ├── .gitattributes
│ │ │ │ ├── car/
│ │ │ │ │ ├── combined_naked_roots_genesis_and_128.car
│ │ │ │ │ ├── lotus_devnet_genesis.car
│ │ │ │ │ ├── lotus_devnet_genesis_shuffled_nulroot.car
│ │ │ │ │ ├── lotus_testnet_export_128.car
│ │ │ │ │ └── lotus_testnet_export_256_multiroot.car
│ │ │ │ ├── hidden-files-folder/
│ │ │ │ │ ├── .hiddenTest.txt
│ │ │ │ │ ├── alice.txt
│ │ │ │ │ ├── files/
│ │ │ │ │ │ ├── hello.txt
│ │ │ │ │ │ └── ipfs.txt
│ │ │ │ │ ├── hello-link
│ │ │ │ │ ├── holmes.txt
│ │ │ │ │ ├── ipfs-add.js
│ │ │ │ │ ├── jungle.txt
│ │ │ │ │ └── pp.txt
│ │ │ │ ├── refs-test/
│ │ │ │ │ ├── animals/
│ │ │ │ │ │ ├── land/
│ │ │ │ │ │ │ ├── african.txt
│ │ │ │ │ │ │ ├── americas.txt
│ │ │ │ │ │ │ └── australian.txt
│ │ │ │ │ │ └── sea/
│ │ │ │ │ │ ├── atlantic.txt
│ │ │ │ │ │ └── indian.txt
│ │ │ │ │ ├── atlantic-animals
│ │ │ │ │ ├── fruits/
│ │ │ │ │ │ └── tropical.txt
│ │ │ │ │ └── mushroom.txt
│ │ │ │ ├── ssl/
│ │ │ │ │ ├── cert.pem
│ │ │ │ │ └── privkey.pem
│ │ │ │ ├── test-folder/
│ │ │ │ │ ├── alice.txt
│ │ │ │ │ ├── files/
│ │ │ │ │ │ ├── hello.txt
│ │ │ │ │ │ └── ipfs.txt
│ │ │ │ │ ├── holmes.txt
│ │ │ │ │ ├── ipfs-add.js
│ │ │ │ │ ├── jungle.txt
│ │ │ │ │ └── pp.txt
│ │ │ │ └── weird name folder [v0]/
│ │ │ │ ├── add
│ │ │ │ ├── cat
│ │ │ │ ├── files/
│ │ │ │ │ ├── hello.txt
│ │ │ │ │ └── ipfs.txt
│ │ │ │ ├── hello-link
│ │ │ │ ├── ipfs-add
│ │ │ │ ├── ls
│ │ │ │ └── version
│ │ │ └── interface.spec.js
│ │ └── tsconfig.json
│ ├── ipfs/
│ │ ├── .aegir.js
│ │ ├── CHANGELOG.md
│ │ ├── CODE_OF_CONDUCT.md
│ │ ├── CONTRIBUTING.md
│ │ ├── COPYRIGHT
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── Makefile
│ │ ├── README.md
│ │ ├── init-and-daemon.sh
│ │ ├── maintainer.json
│ │ ├── package.json
│ │ ├── scripts/
│ │ │ └── update-version.js
│ │ ├── src/
│ │ │ ├── cli.js
│ │ │ ├── index.js
│ │ │ ├── package.js
│ │ │ ├── path.browser.js
│ │ │ └── path.js
│ │ ├── test/
│ │ │ ├── interface-client.js
│ │ │ ├── interface-core.js
│ │ │ ├── interface-http-go.js
│ │ │ ├── interface-http-js.js
│ │ │ └── utils/
│ │ │ ├── factory.js
│ │ │ ├── mock-pinning-service.js
│ │ │ └── mock-preload-node.js
│ │ └── tsconfig.json
│ ├── ipfs-cli/
│ │ ├── CHANGELOG.md
│ │ ├── CODE_OF_CONDUCT.md
│ │ ├── CONTRIBUTING.md
│ │ ├── COPYRIGHT
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── command-alias.js
│ │ │ ├── commands/
│ │ │ │ ├── add.js
│ │ │ │ ├── bitswap/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── stat.js
│ │ │ │ │ ├── unwant.js
│ │ │ │ │ └── wantlist.js
│ │ │ │ ├── bitswap.js
│ │ │ │ ├── block/
│ │ │ │ │ ├── get.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── put.js
│ │ │ │ │ ├── rm.js
│ │ │ │ │ └── stat.js
│ │ │ │ ├── block.js
│ │ │ │ ├── bootstrap/
│ │ │ │ │ ├── add.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── list.js
│ │ │ │ │ └── rm.js
│ │ │ │ ├── bootstrap.js
│ │ │ │ ├── cat.js
│ │ │ │ ├── cid/
│ │ │ │ │ ├── base32.js
│ │ │ │ │ ├── bases.js
│ │ │ │ │ ├── codecs.js
│ │ │ │ │ ├── format.js
│ │ │ │ │ ├── hashes.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── cid.js
│ │ │ │ ├── config/
│ │ │ │ │ ├── edit.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── profile/
│ │ │ │ │ │ ├── apply.js
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── ls.js
│ │ │ │ │ ├── profile.js
│ │ │ │ │ ├── replace.js
│ │ │ │ │ └── show.js
│ │ │ │ ├── config.js
│ │ │ │ ├── daemon.js
│ │ │ │ ├── dag/
│ │ │ │ │ ├── export.js
│ │ │ │ │ ├── get.js
│ │ │ │ │ ├── import.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── put.js
│ │ │ │ │ └── resolve.js
│ │ │ │ ├── dag.js
│ │ │ │ ├── dht/
│ │ │ │ │ ├── find-peer.js
│ │ │ │ │ ├── find-providers.js
│ │ │ │ │ ├── get.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── provide.js
│ │ │ │ │ ├── put.js
│ │ │ │ │ └── query.js
│ │ │ │ ├── dht.js
│ │ │ │ ├── dns.js
│ │ │ │ ├── files/
│ │ │ │ │ ├── chmod.js
│ │ │ │ │ ├── cp.js
│ │ │ │ │ ├── flush.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── ls.js
│ │ │ │ │ ├── mkdir.js
│ │ │ │ │ ├── mv.js
│ │ │ │ │ ├── read.js
│ │ │ │ │ ├── rm.js
│ │ │ │ │ ├── stat.js
│ │ │ │ │ ├── touch.js
│ │ │ │ │ └── write.js
│ │ │ │ ├── files.js
│ │ │ │ ├── get.js
│ │ │ │ ├── id.js
│ │ │ │ ├── index.js
│ │ │ │ ├── init.js
│ │ │ │ ├── key/
│ │ │ │ │ ├── export.js
│ │ │ │ │ ├── gen.js
│ │ │ │ │ ├── import.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── list.js
│ │ │ │ │ ├── rename.js
│ │ │ │ │ └── rm.js
│ │ │ │ ├── key.js
│ │ │ │ ├── ls.js
│ │ │ │ ├── name/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── publish.js
│ │ │ │ │ ├── pubsub/
│ │ │ │ │ │ ├── cancel.js
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ ├── state.js
│ │ │ │ │ │ └── subs.js
│ │ │ │ │ ├── pubsub.js
│ │ │ │ │ └── resolve.js
│ │ │ │ ├── name.js
│ │ │ │ ├── object/
│ │ │ │ │ ├── data.js
│ │ │ │ │ ├── get.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── links.js
│ │ │ │ │ ├── new.js
│ │ │ │ │ ├── patch/
│ │ │ │ │ │ ├── add-link.js
│ │ │ │ │ │ ├── append-data.js
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ ├── rm-link.js
│ │ │ │ │ │ └── set-data.js
│ │ │ │ │ ├── patch.js
│ │ │ │ │ ├── put.js
│ │ │ │ │ └── stat.js
│ │ │ │ ├── object.js
│ │ │ │ ├── pin/
│ │ │ │ │ ├── add.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── ls.js
│ │ │ │ │ └── rm.js
│ │ │ │ ├── pin.js
│ │ │ │ ├── ping.js
│ │ │ │ ├── pubsub/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── ls.js
│ │ │ │ │ ├── peers.js
│ │ │ │ │ ├── pub.js
│ │ │ │ │ └── sub.js
│ │ │ │ ├── pubsub.js
│ │ │ │ ├── refs-local.js
│ │ │ │ ├── refs.js
│ │ │ │ ├── repo/
│ │ │ │ │ ├── gc.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── stat.js
│ │ │ │ │ └── version.js
│ │ │ │ ├── repo.js
│ │ │ │ ├── resolve.js
│ │ │ │ ├── shutdown.js
│ │ │ │ ├── stats/
│ │ │ │ │ ├── bitswap.js
│ │ │ │ │ ├── bw.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── repo.js
│ │ │ │ ├── stats.js
│ │ │ │ ├── swarm/
│ │ │ │ │ ├── addrs/
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── local.js
│ │ │ │ │ ├── addrs.js
│ │ │ │ │ ├── connect.js
│ │ │ │ │ ├── disconnect.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── peers.js
│ │ │ │ ├── swarm.js
│ │ │ │ └── version.js
│ │ │ ├── index.js
│ │ │ ├── parser.js
│ │ │ ├── types.ts
│ │ │ └── utils.js
│ │ ├── test/
│ │ │ ├── add.spec.js
│ │ │ ├── bitswap.spec.js
│ │ │ ├── block.spec.js
│ │ │ ├── bootstrap.spec.js
│ │ │ ├── cat.spec.js
│ │ │ ├── cid.spec.js
│ │ │ ├── config.spec.js
│ │ │ ├── daemon.spec.js
│ │ │ ├── dag.spec.js
│ │ │ ├── dht.spec.js
│ │ │ ├── dns.spec.js
│ │ │ ├── files/
│ │ │ │ ├── chmod.js
│ │ │ │ ├── cp.js
│ │ │ │ ├── flush.js
│ │ │ │ ├── ls.js
│ │ │ │ ├── mkdir.js
│ │ │ │ ├── mv.js
│ │ │ │ ├── read.js
│ │ │ │ ├── rm.js
│ │ │ │ ├── stat.js
│ │ │ │ ├── touch.js
│ │ │ │ └── write.js
│ │ │ ├── general.spec.js
│ │ │ ├── get.spec.js
│ │ │ ├── id.spec.js
│ │ │ ├── init.spec.js
│ │ │ ├── key.spec.js
│ │ │ ├── ls.spec.js
│ │ │ ├── name-pubsub.spec.js
│ │ │ ├── name.spec.js
│ │ │ ├── object.spec.js
│ │ │ ├── pin.spec.js
│ │ │ ├── ping.spec.js
│ │ │ ├── progress-bar.spec.js
│ │ │ ├── pubsub.spec.js
│ │ │ ├── refs-local.spec.js
│ │ │ ├── refs.spec.js
│ │ │ ├── repo.spec.js
│ │ │ ├── resolve.spec.js
│ │ │ ├── swarm.spec.js
│ │ │ ├── utils/
│ │ │ │ ├── clean.js
│ │ │ │ ├── cli.js
│ │ │ │ ├── ipfs-exec.js
│ │ │ │ ├── match-iterable.js
│ │ │ │ ├── match-peer-id.js
│ │ │ │ └── platforms.js
│ │ │ └── version.spec.js
│ │ └── tsconfig.json
│ ├── ipfs-client/
│ │ ├── .aegir.js
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.js
│ │ └── tsconfig.json
│ ├── ipfs-core/
│ │ ├── .aegir.js
│ │ ├── CHANGELOG.md
│ │ ├── CODE_OF_CONDUCT.md
│ │ ├── CONTRIBUTING.md
│ │ ├── COPYRIGHT
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── scripts/
│ │ │ └── update-version.js
│ │ ├── src/
│ │ │ ├── block-storage.js
│ │ │ ├── components/
│ │ │ │ ├── add-all/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── utils.js
│ │ │ │ ├── add.js
│ │ │ │ ├── bitswap/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── stat.js
│ │ │ │ │ ├── unwant.js
│ │ │ │ │ ├── wantlist-for-peer.js
│ │ │ │ │ └── wantlist.js
│ │ │ │ ├── block/
│ │ │ │ │ ├── get.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── put.js
│ │ │ │ │ ├── rm.js
│ │ │ │ │ ├── stat.js
│ │ │ │ │ └── utils.js
│ │ │ │ ├── bootstrap/
│ │ │ │ │ ├── add.js
│ │ │ │ │ ├── clear.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── list.js
│ │ │ │ │ ├── reset.js
│ │ │ │ │ ├── rm.js
│ │ │ │ │ └── utils.js
│ │ │ │ ├── cat.js
│ │ │ │ ├── config/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── profiles.js
│ │ │ │ ├── dag/
│ │ │ │ │ ├── export.js
│ │ │ │ │ ├── get.js
│ │ │ │ │ ├── import.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── put.js
│ │ │ │ │ └── resolve.js
│ │ │ │ ├── dht.js
│ │ │ │ ├── dns.js
│ │ │ │ ├── files/
│ │ │ │ │ ├── chmod.js
│ │ │ │ │ ├── cp.js
│ │ │ │ │ ├── flush.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── ls.js
│ │ │ │ │ ├── mkdir.js
│ │ │ │ │ ├── mv.js
│ │ │ │ │ ├── read.js
│ │ │ │ │ ├── rm.js
│ │ │ │ │ ├── stat.js
│ │ │ │ │ ├── touch.js
│ │ │ │ │ ├── utils/
│ │ │ │ │ │ ├── add-link.js
│ │ │ │ │ │ ├── create-lock.js
│ │ │ │ │ │ ├── create-node.js
│ │ │ │ │ │ ├── dir-sharded.js
│ │ │ │ │ │ ├── hamt-constants.js
│ │ │ │ │ │ ├── hamt-utils.js
│ │ │ │ │ │ ├── persist.js
│ │ │ │ │ │ ├── remove-link.js
│ │ │ │ │ │ ├── to-async-iterator.js
│ │ │ │ │ │ ├── to-mfs-path.js
│ │ │ │ │ │ ├── to-path-components.js
│ │ │ │ │ │ ├── to-trail.js
│ │ │ │ │ │ ├── update-mfs-root.js
│ │ │ │ │ │ ├── update-tree.js
│ │ │ │ │ │ └── with-mfs-root.js
│ │ │ │ │ └── write.js
│ │ │ │ ├── get.js
│ │ │ │ ├── id.js
│ │ │ │ ├── index.js
│ │ │ │ ├── ipns.js
│ │ │ │ ├── is-online.js
│ │ │ │ ├── key/
│ │ │ │ │ ├── export.js
│ │ │ │ │ ├── gen.js
│ │ │ │ │ ├── import.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── info.js
│ │ │ │ │ ├── list.js
│ │ │ │ │ ├── rename.js
│ │ │ │ │ └── rm.js
│ │ │ │ ├── libp2p.js
│ │ │ │ ├── ls.js
│ │ │ │ ├── name/
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── publish.js
│ │ │ │ │ ├── pubsub/
│ │ │ │ │ │ ├── cancel.js
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ ├── state.js
│ │ │ │ │ │ ├── subs.js
│ │ │ │ │ │ └── utils.js
│ │ │ │ │ ├── resolve.js
│ │ │ │ │ └── utils.js
│ │ │ │ ├── network.js
│ │ │ │ ├── object/
│ │ │ │ │ ├── data.js
│ │ │ │ │ ├── get.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── links.js
│ │ │ │ │ ├── new.js
│ │ │ │ │ ├── patch/
│ │ │ │ │ │ ├── add-link.js
│ │ │ │ │ │ ├── append-data.js
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ ├── rm-link.js
│ │ │ │ │ │ └── set-data.js
│ │ │ │ │ ├── put.js
│ │ │ │ │ └── stat.js
│ │ │ │ ├── pin/
│ │ │ │ │ ├── add-all.js
│ │ │ │ │ ├── add.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── ls.js
│ │ │ │ │ ├── rm-all.js
│ │ │ │ │ └── rm.js
│ │ │ │ ├── ping.js
│ │ │ │ ├── pubsub.js
│ │ │ │ ├── refs/
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── local.js
│ │ │ │ ├── repo/
│ │ │ │ │ ├── gc.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── stat.js
│ │ │ │ │ └── version.js
│ │ │ │ ├── resolve.js
│ │ │ │ ├── root.js
│ │ │ │ ├── start.js
│ │ │ │ ├── stats/
│ │ │ │ │ ├── bw.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── stop.js
│ │ │ │ ├── storage.js
│ │ │ │ ├── swarm/
│ │ │ │ │ ├── addrs.js
│ │ │ │ │ ├── connect.js
│ │ │ │ │ ├── disconnect.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── local-addrs.js
│ │ │ │ │ └── peers.js
│ │ │ │ └── version.js
│ │ │ ├── errors.js
│ │ │ ├── index.js
│ │ │ ├── ipns/
│ │ │ │ ├── index.js
│ │ │ │ ├── publisher.js
│ │ │ │ ├── republisher.js
│ │ │ │ ├── resolver.js
│ │ │ │ └── routing/
│ │ │ │ ├── config.js
│ │ │ │ ├── dht-datastore.js
│ │ │ │ ├── offline-datastore.js
│ │ │ │ └── pubsub-datastore.js
│ │ │ ├── mfs-preload.js
│ │ │ ├── preload.js
│ │ │ ├── types.ts
│ │ │ ├── utils/
│ │ │ │ ├── service.js
│ │ │ │ └── tlru.js
│ │ │ ├── utils.js
│ │ │ └── version.js
│ │ ├── test/
│ │ │ ├── add-all.spec.js
│ │ │ ├── block-storage.spec.js
│ │ │ ├── bootstrappers.js
│ │ │ ├── config.spec.js
│ │ │ ├── create-node.spec.js
│ │ │ ├── init.spec.js
│ │ │ ├── ipld.spec.js
│ │ │ ├── key-exchange.spec.js
│ │ │ ├── libp2p.spec.js
│ │ │ ├── mfs-preload.spec.js
│ │ │ ├── name.spec.js
│ │ │ ├── preload.spec.js
│ │ │ ├── pubsub.spec.js
│ │ │ ├── utils/
│ │ │ │ ├── clean.js
│ │ │ │ ├── codecs.js
│ │ │ │ ├── create-backend.js
│ │ │ │ ├── create-node.js
│ │ │ │ ├── create-repo.js
│ │ │ │ ├── mock-preload-node-utils.js
│ │ │ │ ├── mock-preload-node.js
│ │ │ │ └── wait-for.js
│ │ │ └── utils.spec.js
│ │ └── tsconfig.json
│ ├── ipfs-core-config/
│ │ ├── .aegir.js
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── config.browser.js
│ │ │ ├── config.js
│ │ │ ├── dns.browser.js
│ │ │ ├── dns.js
│ │ │ ├── index.js
│ │ │ ├── init-assets.browser.js
│ │ │ ├── init-assets.js
│ │ │ ├── init-files/
│ │ │ │ └── init-docs/
│ │ │ │ ├── about.js
│ │ │ │ ├── contact.js
│ │ │ │ ├── docs/
│ │ │ │ │ └── index.js
│ │ │ │ ├── help.js
│ │ │ │ ├── index.js
│ │ │ │ ├── quick-start.js
│ │ │ │ ├── readme.js
│ │ │ │ ├── security-notes.js
│ │ │ │ └── tour/
│ │ │ │ └── 0.0-intro.js
│ │ │ ├── libp2p-pubsub-routers.browser.js
│ │ │ ├── libp2p-pubsub-routers.js
│ │ │ ├── libp2p.browser.js
│ │ │ ├── libp2p.js
│ │ │ ├── preload.browser.js
│ │ │ ├── preload.js
│ │ │ ├── repo.browser.js
│ │ │ ├── repo.js
│ │ │ └── utils/
│ │ │ ├── lru-datastore.js
│ │ │ └── tlru.js
│ │ └── tsconfig.json
│ ├── ipfs-core-types/
│ │ ├── CHANGELOG.md
│ │ ├── COPYRIGHT
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── bitswap/
│ │ │ │ └── index.ts
│ │ │ ├── block/
│ │ │ │ └── index.ts
│ │ │ ├── bootstrap/
│ │ │ │ └── index.ts
│ │ │ ├── config/
│ │ │ │ ├── index.ts
│ │ │ │ └── profiles/
│ │ │ │ └── index.ts
│ │ │ ├── dag/
│ │ │ │ └── index.ts
│ │ │ ├── dht/
│ │ │ │ └── index.ts
│ │ │ ├── diag/
│ │ │ │ └── index.ts
│ │ │ ├── files/
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── key/
│ │ │ │ └── index.ts
│ │ │ ├── log/
│ │ │ │ └── index.ts
│ │ │ ├── name/
│ │ │ │ ├── index.ts
│ │ │ │ └── pubsub/
│ │ │ │ └── index.ts
│ │ │ ├── object/
│ │ │ │ ├── index.ts
│ │ │ │ └── patch/
│ │ │ │ └── index.ts
│ │ │ ├── pin/
│ │ │ │ ├── index.ts
│ │ │ │ └── remote/
│ │ │ │ ├── index.ts
│ │ │ │ └── service/
│ │ │ │ └── index.ts
│ │ │ ├── pubsub/
│ │ │ │ └── index.ts
│ │ │ ├── refs/
│ │ │ │ └── index.ts
│ │ │ ├── repo/
│ │ │ │ └── index.ts
│ │ │ ├── root.ts
│ │ │ ├── stats/
│ │ │ │ └── index.ts
│ │ │ ├── swarm/
│ │ │ │ └── index.ts
│ │ │ └── utils.ts
│ │ └── tsconfig.json
│ ├── ipfs-core-utils/
│ │ ├── .aegir.js
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── agent.browser.js
│ │ │ ├── agent.js
│ │ │ ├── errors.js
│ │ │ ├── files/
│ │ │ │ ├── format-mode.js
│ │ │ │ ├── format-mtime.js
│ │ │ │ ├── normalise-candidate-multiple.js
│ │ │ │ ├── normalise-candidate-single.js
│ │ │ │ ├── normalise-content.browser.js
│ │ │ │ ├── normalise-content.js
│ │ │ │ ├── normalise-input-multiple.browser.js
│ │ │ │ ├── normalise-input-multiple.js
│ │ │ │ ├── normalise-input-single.browser.js
│ │ │ │ ├── normalise-input-single.js
│ │ │ │ └── utils.js
│ │ │ ├── index.js
│ │ │ ├── mode-to-string.js
│ │ │ ├── multibases.js
│ │ │ ├── multicodecs.js
│ │ │ ├── multihashes.js
│ │ │ ├── multipart-request.browser.js
│ │ │ ├── multipart-request.js
│ │ │ ├── multipart-request.node.js
│ │ │ ├── pins/
│ │ │ │ └── normalise-input.js
│ │ │ ├── to-cid-and-path.js
│ │ │ ├── to-url-string.js
│ │ │ ├── types.ts
│ │ │ └── with-timeout-option.js
│ │ ├── test/
│ │ │ ├── files/
│ │ │ │ ├── format-mode.spec.js
│ │ │ │ ├── format-mtime.spec.js
│ │ │ │ ├── normalise-input-multiple.spec.js
│ │ │ │ └── normalise-input-single.spec.js
│ │ │ ├── fixtures/
│ │ │ │ └── file.txt
│ │ │ ├── pins/
│ │ │ │ └── normalise-input.spec.js
│ │ │ └── tests.spec.js
│ │ └── tsconfig.json
│ ├── ipfs-daemon/
│ │ ├── CHANGELOG.md
│ │ ├── CODE_OF_CONDUCT.md
│ │ ├── CONTRIBUTING.md
│ │ ├── COPYRIGHT
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.js
│ │ ├── test/
│ │ │ └── index.spec.js
│ │ └── tsconfig.json
│ ├── ipfs-grpc-client/
│ │ ├── .aegir.js
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── core-api/
│ │ │ │ ├── add-all.js
│ │ │ │ ├── files/
│ │ │ │ │ ├── ls.js
│ │ │ │ │ └── write.js
│ │ │ │ ├── id.js
│ │ │ │ └── pubsub/
│ │ │ │ ├── subscribe.js
│ │ │ │ ├── subscriptions.js
│ │ │ │ └── unsubscribe.js
│ │ │ ├── grpc/
│ │ │ │ ├── transport.browser.js
│ │ │ │ ├── transport.js
│ │ │ │ └── transport.node.js
│ │ │ ├── index.js
│ │ │ ├── types.ts
│ │ │ └── utils/
│ │ │ ├── bidi-to-duplex.js
│ │ │ ├── client-stream-to-promise.js
│ │ │ ├── load-services.js
│ │ │ ├── server-stream-to-iterator.js
│ │ │ ├── to-headers.js
│ │ │ └── unary-to-promise.js
│ │ ├── test/
│ │ │ ├── agent.js
│ │ │ ├── node.js
│ │ │ └── utils.spec.js
│ │ └── tsconfig.json
│ ├── ipfs-grpc-protocol/
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── scripts/
│ │ │ └── update-index.js
│ │ ├── src/
│ │ │ ├── common.proto
│ │ │ ├── index.js
│ │ │ ├── mfs.proto
│ │ │ ├── pubsub.proto
│ │ │ └── root.proto
│ │ └── tsconfig.json
│ ├── ipfs-grpc-server/
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── endpoints/
│ │ │ │ ├── add.js
│ │ │ │ ├── id.js
│ │ │ │ ├── mfs/
│ │ │ │ │ ├── ls.js
│ │ │ │ │ └── write.js
│ │ │ │ └── pubsub/
│ │ │ │ ├── subscribe.js
│ │ │ │ ├── subscriptions.js
│ │ │ │ └── unsubscribe.js
│ │ │ ├── index.js
│ │ │ ├── types.ts
│ │ │ └── utils/
│ │ │ ├── encode-mtime.js
│ │ │ ├── load-services.js
│ │ │ ├── web-socket-message-channel.js
│ │ │ └── web-socket-server.js
│ │ ├── test/
│ │ │ ├── add.spec.js
│ │ │ ├── id.spec.js
│ │ │ ├── mfs/
│ │ │ │ ├── ls.spec.js
│ │ │ │ └── write.spec.js
│ │ │ └── utils/
│ │ │ ├── channel.js
│ │ │ └── server.js
│ │ └── tsconfig.json
│ ├── ipfs-http-client/
│ │ ├── .aegir.js
│ │ ├── CHANGELOG.md
│ │ ├── CONTRIBUTING.md
│ │ ├── COPYRIGHT
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── maintainer.json
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── add-all.js
│ │ │ ├── add.js
│ │ │ ├── bitswap/
│ │ │ │ ├── index.js
│ │ │ │ ├── stat.js
│ │ │ │ ├── unwant.js
│ │ │ │ ├── wantlist-for-peer.js
│ │ │ │ └── wantlist.js
│ │ │ ├── block/
│ │ │ │ ├── get.js
│ │ │ │ ├── index.js
│ │ │ │ ├── put.js
│ │ │ │ ├── rm.js
│ │ │ │ └── stat.js
│ │ │ ├── bootstrap/
│ │ │ │ ├── add.js
│ │ │ │ ├── clear.js
│ │ │ │ ├── index.js
│ │ │ │ ├── list.js
│ │ │ │ ├── reset.js
│ │ │ │ └── rm.js
│ │ │ ├── cat.js
│ │ │ ├── commands.js
│ │ │ ├── config/
│ │ │ │ ├── get-all.js
│ │ │ │ ├── get.js
│ │ │ │ ├── index.js
│ │ │ │ ├── profiles/
│ │ │ │ │ ├── apply.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── list.js
│ │ │ │ ├── replace.js
│ │ │ │ └── set.js
│ │ │ ├── dag/
│ │ │ │ ├── export.js
│ │ │ │ ├── get.js
│ │ │ │ ├── import.js
│ │ │ │ ├── index.js
│ │ │ │ ├── put.js
│ │ │ │ └── resolve.js
│ │ │ ├── dht/
│ │ │ │ ├── find-peer.js
│ │ │ │ ├── find-provs.js
│ │ │ │ ├── get.js
│ │ │ │ ├── index.js
│ │ │ │ ├── map-event.js
│ │ │ │ ├── provide.js
│ │ │ │ ├── put.js
│ │ │ │ ├── query.js
│ │ │ │ └── response-types.js
│ │ │ ├── diag/
│ │ │ │ ├── cmds.js
│ │ │ │ ├── index.js
│ │ │ │ ├── net.js
│ │ │ │ └── sys.js
│ │ │ ├── dns.js
│ │ │ ├── files/
│ │ │ │ ├── chmod.js
│ │ │ │ ├── cp.js
│ │ │ │ ├── flush.js
│ │ │ │ ├── index.js
│ │ │ │ ├── ls.js
│ │ │ │ ├── mkdir.js
│ │ │ │ ├── mv.js
│ │ │ │ ├── read.js
│ │ │ │ ├── rm.js
│ │ │ │ ├── stat.js
│ │ │ │ ├── touch.js
│ │ │ │ └── write.js
│ │ │ ├── get-endpoint-config.js
│ │ │ ├── get.js
│ │ │ ├── id.js
│ │ │ ├── index.js
│ │ │ ├── is-online.js
│ │ │ ├── key/
│ │ │ │ ├── export.js
│ │ │ │ ├── gen.js
│ │ │ │ ├── import.js
│ │ │ │ ├── index.js
│ │ │ │ ├── info.js
│ │ │ │ ├── list.js
│ │ │ │ ├── rename.js
│ │ │ │ └── rm.js
│ │ │ ├── lib/
│ │ │ │ ├── abort-signal.js
│ │ │ │ ├── configure.js
│ │ │ │ ├── core.js
│ │ │ │ ├── http-rpc-wire-format.js
│ │ │ │ ├── mode-to-string.js
│ │ │ │ ├── object-to-camel-with-metadata.js
│ │ │ │ ├── object-to-camel.js
│ │ │ │ ├── parse-mtime.js
│ │ │ │ ├── resolve.js
│ │ │ │ └── to-url-search-params.js
│ │ │ ├── log/
│ │ │ │ ├── index.js
│ │ │ │ ├── level.js
│ │ │ │ ├── ls.js
│ │ │ │ └── tail.js
│ │ │ ├── ls.js
│ │ │ ├── mount.js
│ │ │ ├── name/
│ │ │ │ ├── index.js
│ │ │ │ ├── publish.js
│ │ │ │ ├── pubsub/
│ │ │ │ │ ├── cancel.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── state.js
│ │ │ │ │ └── subs.js
│ │ │ │ └── resolve.js
│ │ │ ├── object/
│ │ │ │ ├── data.js
│ │ │ │ ├── get.js
│ │ │ │ ├── index.js
│ │ │ │ ├── links.js
│ │ │ │ ├── new.js
│ │ │ │ ├── patch/
│ │ │ │ │ ├── add-link.js
│ │ │ │ │ ├── append-data.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── rm-link.js
│ │ │ │ │ └── set-data.js
│ │ │ │ ├── put.js
│ │ │ │ └── stat.js
│ │ │ ├── pin/
│ │ │ │ ├── add-all.js
│ │ │ │ ├── add.js
│ │ │ │ ├── index.js
│ │ │ │ ├── ls.js
│ │ │ │ ├── remote/
│ │ │ │ │ ├── add.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── ls.js
│ │ │ │ │ ├── rm-all.js
│ │ │ │ │ ├── rm.js
│ │ │ │ │ ├── service/
│ │ │ │ │ │ ├── add.js
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ ├── ls.js
│ │ │ │ │ │ ├── rm.js
│ │ │ │ │ │ └── utils.js
│ │ │ │ │ └── utils.js
│ │ │ │ ├── rm-all.js
│ │ │ │ └── rm.js
│ │ │ ├── ping.js
│ │ │ ├── pubsub/
│ │ │ │ ├── index.js
│ │ │ │ ├── ls.js
│ │ │ │ ├── peers.js
│ │ │ │ ├── publish.js
│ │ │ │ ├── subscribe.js
│ │ │ │ ├── subscription-tracker.js
│ │ │ │ └── unsubscribe.js
│ │ │ ├── refs/
│ │ │ │ ├── index.js
│ │ │ │ └── local.js
│ │ │ ├── repo/
│ │ │ │ ├── gc.js
│ │ │ │ ├── index.js
│ │ │ │ ├── stat.js
│ │ │ │ └── version.js
│ │ │ ├── resolve.js
│ │ │ ├── start.js
│ │ │ ├── stats/
│ │ │ │ ├── bw.js
│ │ │ │ └── index.js
│ │ │ ├── stop.js
│ │ │ ├── swarm/
│ │ │ │ ├── addrs.js
│ │ │ │ ├── connect.js
│ │ │ │ ├── disconnect.js
│ │ │ │ ├── index.js
│ │ │ │ ├── local-addrs.js
│ │ │ │ └── peers.js
│ │ │ ├── types.ts
│ │ │ └── version.js
│ │ ├── test/
│ │ │ ├── commands.spec.js
│ │ │ ├── constructor.spec.js
│ │ │ ├── dag.spec.js
│ │ │ ├── diag.spec.js
│ │ │ ├── endpoint-config.spec.js
│ │ │ ├── exports.spec.js
│ │ │ ├── files.spec.js
│ │ │ ├── fixtures/
│ │ │ │ ├── .gitattributes
│ │ │ │ ├── 15mb.random
│ │ │ │ ├── r-config.json
│ │ │ │ ├── ssl/
│ │ │ │ │ ├── cert.pem
│ │ │ │ │ └── privkey.pem
│ │ │ │ ├── test-folder/
│ │ │ │ │ ├── .hiddenTest.txt
│ │ │ │ │ ├── add
│ │ │ │ │ ├── cat
│ │ │ │ │ ├── files/
│ │ │ │ │ │ ├── hello.txt
│ │ │ │ │ │ └── ipfs.txt
│ │ │ │ │ ├── ipfs-add
│ │ │ │ │ ├── ls
│ │ │ │ │ └── version
│ │ │ │ ├── testconfig.json
│ │ │ │ └── testfile.txt
│ │ │ ├── key.spec.js
│ │ │ ├── lib.error-handler.spec.js
│ │ │ ├── log.spec.js
│ │ │ ├── node/
│ │ │ │ ├── agent.js
│ │ │ │ ├── custom-headers.js
│ │ │ │ ├── request-api.js
│ │ │ │ └── swarm.js
│ │ │ ├── node.js
│ │ │ ├── ping.spec.js
│ │ │ ├── pubsub.spec.js
│ │ │ ├── repo.spec.js
│ │ │ ├── stats.spec.js
│ │ │ └── utils/
│ │ │ ├── factory.js
│ │ │ └── throws-async.js
│ │ └── tsconfig.json
│ ├── ipfs-http-gateway/
│ │ ├── CHANGELOG.md
│ │ ├── CODE_OF_CONDUCT.md
│ │ ├── CONTRIBUTING.md
│ │ ├── COPYRIGHT
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.js
│ │ │ ├── resources/
│ │ │ │ ├── gateway.js
│ │ │ │ └── index.js
│ │ │ ├── routes/
│ │ │ │ ├── gateway.js
│ │ │ │ └── index.js
│ │ │ ├── types.ts
│ │ │ └── utils/
│ │ │ └── path.js
│ │ ├── test/
│ │ │ ├── fixtures/
│ │ │ │ ├── index.html
│ │ │ │ └── nested-folder/
│ │ │ │ ├── hello.txt
│ │ │ │ ├── ipfs.txt
│ │ │ │ └── nested.html
│ │ │ ├── routes.spec.js
│ │ │ └── utils/
│ │ │ └── http.js
│ │ └── tsconfig.json
│ ├── ipfs-http-response/
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── dir-view/
│ │ │ │ ├── index.js
│ │ │ │ └── style.js
│ │ │ ├── index.js
│ │ │ ├── resolver.js
│ │ │ └── utils/
│ │ │ ├── content-type.js
│ │ │ └── path.js
│ │ ├── test/
│ │ │ ├── fixtures/
│ │ │ │ ├── .gitattributes
│ │ │ │ ├── test-folder/
│ │ │ │ │ ├── files/
│ │ │ │ │ │ └── hello.txt
│ │ │ │ │ ├── holmes.txt
│ │ │ │ │ └── pp.txt
│ │ │ │ ├── test-mime-types/
│ │ │ │ │ ├── index.html
│ │ │ │ │ └── pp.txt
│ │ │ │ ├── test-site/
│ │ │ │ │ ├── holmes.txt
│ │ │ │ │ ├── index.html
│ │ │ │ │ └── pp.txt
│ │ │ │ └── testfile.txt
│ │ │ ├── index.spec.js
│ │ │ ├── resolver.spec.js
│ │ │ └── utils/
│ │ │ └── web-response-env.js
│ │ └── tsconfig.json
│ ├── ipfs-http-server/
│ │ ├── CHANGELOG.md
│ │ ├── CODE_OF_CONDUCT.md
│ │ ├── CONTRIBUTING.md
│ │ ├── COPYRIGHT
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── scripts/
│ │ │ └── update-version.js
│ │ ├── src/
│ │ │ ├── api/
│ │ │ │ ├── resources/
│ │ │ │ │ ├── bitswap.js
│ │ │ │ │ ├── block.js
│ │ │ │ │ ├── bootstrap.js
│ │ │ │ │ ├── config.js
│ │ │ │ │ ├── dag.js
│ │ │ │ │ ├── dht.js
│ │ │ │ │ ├── dns.js
│ │ │ │ │ ├── files/
│ │ │ │ │ │ ├── chmod.js
│ │ │ │ │ │ ├── cp.js
│ │ │ │ │ │ ├── flush.js
│ │ │ │ │ │ ├── ls.js
│ │ │ │ │ │ ├── mkdir.js
│ │ │ │ │ │ ├── mv.js
│ │ │ │ │ │ ├── read.js
│ │ │ │ │ │ ├── rm.js
│ │ │ │ │ │ ├── stat.js
│ │ │ │ │ │ ├── touch.js
│ │ │ │ │ │ ├── utils/
│ │ │ │ │ │ │ └── parse-mtime.js
│ │ │ │ │ │ └── write.js
│ │ │ │ │ ├── files-regular.js
│ │ │ │ │ ├── id.js
│ │ │ │ │ ├── key.js
│ │ │ │ │ ├── name.js
│ │ │ │ │ ├── object.js
│ │ │ │ │ ├── pin.js
│ │ │ │ │ ├── ping.js
│ │ │ │ │ ├── pubsub.js
│ │ │ │ │ ├── repo.js
│ │ │ │ │ ├── resolve.js
│ │ │ │ │ ├── shutdown.js
│ │ │ │ │ ├── stats.js
│ │ │ │ │ ├── swarm.js
│ │ │ │ │ └── version.js
│ │ │ │ └── routes/
│ │ │ │ ├── bitswap.js
│ │ │ │ ├── block.js
│ │ │ │ ├── bootstrap.js
│ │ │ │ ├── config.js
│ │ │ │ ├── dag.js
│ │ │ │ ├── debug.js
│ │ │ │ ├── dht.js
│ │ │ │ ├── dns.js
│ │ │ │ ├── files-regular.js
│ │ │ │ ├── files.js
│ │ │ │ ├── id.js
│ │ │ │ ├── index.js
│ │ │ │ ├── key.js
│ │ │ │ ├── name.js
│ │ │ │ ├── object.js
│ │ │ │ ├── pin.js
│ │ │ │ ├── ping.js
│ │ │ │ ├── pubsub.js
│ │ │ │ ├── repo.js
│ │ │ │ ├── resolve.js
│ │ │ │ ├── shutdown.js
│ │ │ │ ├── stats.js
│ │ │ │ ├── swarm.js
│ │ │ │ ├── version.js
│ │ │ │ └── webui.js
│ │ │ ├── error-handler.js
│ │ │ ├── index.js
│ │ │ ├── types.ts
│ │ │ ├── utils/
│ │ │ │ ├── joi.js
│ │ │ │ ├── multipart-request-parser.js
│ │ │ │ └── stream-response.js
│ │ │ └── version.js
│ │ ├── test/
│ │ │ ├── cors.js
│ │ │ ├── fixtures/
│ │ │ │ └── test-data/
│ │ │ │ ├── badconfig
│ │ │ │ ├── badnode.json
│ │ │ │ └── node.json
│ │ │ ├── inject/
│ │ │ │ ├── bitswap.js
│ │ │ │ ├── block.js
│ │ │ │ ├── bootstrap.js
│ │ │ │ ├── browser-headers.js
│ │ │ │ ├── config.js
│ │ │ │ ├── dag.js
│ │ │ │ ├── dht.js
│ │ │ │ ├── dns.js
│ │ │ │ ├── files.js
│ │ │ │ ├── id.js
│ │ │ │ ├── key.js
│ │ │ │ ├── mfs/
│ │ │ │ │ ├── chmod.js
│ │ │ │ │ ├── cp.js
│ │ │ │ │ ├── flush.js
│ │ │ │ │ ├── ls.js
│ │ │ │ │ ├── mkdir.js
│ │ │ │ │ ├── mv.js
│ │ │ │ │ ├── read.js
│ │ │ │ │ ├── rm.js
│ │ │ │ │ ├── stat.js
│ │ │ │ │ ├── touch.js
│ │ │ │ │ └── write.js
│ │ │ │ ├── mfs.js
│ │ │ │ ├── name.js
│ │ │ │ ├── object.js
│ │ │ │ ├── pin.js
│ │ │ │ ├── ping.js
│ │ │ │ ├── pubsub.js
│ │ │ │ ├── repo.js
│ │ │ │ ├── resolve.js
│ │ │ │ ├── shutdown.js
│ │ │ │ ├── stats.js
│ │ │ │ ├── swarm.js
│ │ │ │ └── version.js
│ │ │ ├── node.js
│ │ │ ├── routes.js
│ │ │ └── utils/
│ │ │ ├── all-ndjson.js
│ │ │ ├── http.js
│ │ │ ├── match-iterable.js
│ │ │ └── test-http-method.js
│ │ └── tsconfig.json
│ ├── ipfs-message-port-client/
│ │ ├── .aegir.js
│ │ ├── CHANGELOG.md
│ │ ├── CODE_OF_CONDUCT.md
│ │ ├── CONTRIBUTING.md
│ │ ├── COPYRIGHT
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── block.js
│ │ │ ├── client/
│ │ │ │ ├── error.js
│ │ │ │ ├── query.js
│ │ │ │ ├── service.js
│ │ │ │ └── transport.js
│ │ │ ├── client.js
│ │ │ ├── core.js
│ │ │ ├── dag.js
│ │ │ ├── files.js
│ │ │ ├── index.js
│ │ │ └── interface.ts
│ │ ├── test/
│ │ │ ├── interface-message-port-client.js
│ │ │ └── util/
│ │ │ ├── client.js
│ │ │ └── worker.js
│ │ └── tsconfig.json
│ ├── ipfs-message-port-protocol/
│ │ ├── .aegir.js
│ │ ├── CHANGELOG.md
│ │ ├── CODE_OF_CONDUCT.md
│ │ ├── CONTRIBUTING.md
│ │ ├── COPYRIGHT
│ │ ├── LICENSE
│ │ ├── LICENSE-APACHE
│ │ ├── LICENSE-MIT
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── block.js
│ │ │ ├── cid.js
│ │ │ ├── core.js
│ │ │ ├── dag.js
│ │ │ ├── data.ts
│ │ │ ├── error.js
│ │ │ ├── files.ts
│ │ │ ├── index.js
│ │ │ ├── root.ts
│ │ │ └── rpc.ts
│ │ ├── test/
│ │ │ ├── block.browser.js
│ │ │ ├── browser.js
│ │ │ ├── cid.browser.js
│ │ │ ├── cid.spec.js
│ │ │ ├── core.browser.js
│ │ │ ├── dag.browser.js
│ │ │ ├── dag.spec.js
│ │ │ ├── node.js
│ │ │ └── util.js
│ │ └── tsconfig.json
│ └── ipfs-message-port-server/
│ ├── .aegir.js
│ ├── CHANGELOG.md
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.md
│ ├── COPYRIGHT
│ ├── LICENSE
│ ├── LICENSE-APACHE
│ ├── LICENSE-MIT
│ ├── README.md
│ ├── package.json
│ ├── src/
│ │ ├── block.js
│ │ ├── core.js
│ │ ├── dag.js
│ │ ├── files.js
│ │ ├── index.js
│ │ ├── server.js
│ │ └── service.js
│ ├── test/
│ │ ├── basic.spec.js
│ │ ├── node.js
│ │ └── transfer.spec.js
│ └── tsconfig.json
└── scripts/
└── node-globals.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
*
================================================
FILE: .editorconfig
================================================
[*]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
================================================
FILE: .gitattributes
================================================
* text=auto
**/test/fixtures/** text eol=lf
**/test/gateway/** text eol=lf
**/src/init-files/** text eol=lf
*.data binary
*.png binary
*.jpg binary
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Getting Help on IPFS
url: https://ipfs.io/help
about: All information about how and where to get help on IPFS
- name: IPFS Official Forum
url: https://discuss.ipfs.io
about: For general questions, support requests and discussions
================================================
FILE: .github/ISSUE_TEMPLATE/open_an_issue.md
================================================
---
name: Open an issue
about: For reporting bugs or errors in the JavaScript IPFS implementation
title: ''
labels: need/triage
assignees: ''
---
- **Version**:
- **Platform**:
- **Subsystem**:
#### Severity:
#### Description:
#### Steps to reproduce the error:
================================================
FILE: .github/config.yml
================================================
# Configuration for welcome - https://github.com/behaviorbot/welcome
# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome
# Comment to be posted to on first time issues
newIssueWelcomeComment: >
Thank you for submitting your first issue to this repository! A maintainer
will be here shortly to triage and review.
In the meantime, please double-check that you have provided all the
necessary information to make this process easy! Any information that can
help save additional round trips is useful! We currently aim to give
initial feedback within **two business days**. If this does not happen, feel
free to leave a comment.
Please keep an eye on how this issue will be labeled, as labels give an
overview of priorities, assignments and additional actions requested by the
maintainers:
- "Priority" labels will show how urgent this is for the team.
- "Status" labels will show if this is ready to be worked on, blocked, or in progress.
- "Need" labels will indicate if additional input or analysis is required.
Finally, remember to use https://discuss.ipfs.io if you just need general
support.
# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome
# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: >
Thank you for submitting this PR!
A maintainer will be here shortly to review it.
We are super grateful, but we are also overloaded! Help us by making sure
that:
* The context for this PR is clear, with relevant discussion, decisions
and stakeholders linked/mentioned.
* Your contribution itself is clear (code comments, self-review for the
rest) and in its best form. Follow the [code contribution
guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md#code-contribution-guidelines)
if they apply.
Getting other community members to do a review would be great help too on
complex PRs (you can ask in the chats/forums). If you are unsure about
something, just leave us a comment.
Next steps:
* A maintainer will triage and assign priority to this PR, commenting on
any missing things and potentially assigning a reviewer for high
priority items.
* The PR gets reviews, discussed and approvals as needed.
* The PR is merged by maintainers when it has been approved and comments addressed.
We currently aim to provide initial feedback/triaging within **two business
days**. Please keep an eye on any labelling actions, as these will indicate
priorities and status of your contribution.
We are very grateful for your contribution!
# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge
# Comment to be posted to on pull requests merged by a first time user
# Currently disabled
#firstPRMergeComment: ""
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
time: "10:00"
open-pull-requests-limit: 0
commit-message:
prefix: "deps"
prefix-development: "deps(dev)"
================================================
FILE: .github/workflows/examples.yml
================================================
name: Examples
on:
push:
branches:
- master
pull_request:
branches:
- '**'
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master
# test-examples:
# name: Test example ${{ matrix.example.name }}
# needs: build
# runs-on: ubuntu-latest
# continue-on-error: true
# strategy:
# matrix:
# example:
# - name: ipfs browser add readable stream
# repo: https://github.com/ipfs-examples/js-ipfs-browser-add-readable-stream.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# - name: ipfs browser angular
# repo: https://github.com/ipfs-examples/js-ipfs-browser-angular.git
# deps: ipfs-core@$PWD/packages/ipfs-core,ipfs-core-types@$PWD/packages/ipfs-core-types
# - name: ipfs browser browserify
# repo: https://github.com/ipfs-examples/js-ipfs-browser-browserify.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# - name: ipfs browser react
# repo: https://github.com/ipfs-examples/js-ipfs-browser-create-react-app.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# - name: ipfs browser exchange files
# repo: https://github.com/ipfs-examples/js-ipfs-browser-exchange-files.git
# deps: ipfs-core@$PWD/packages/ipfs-core,ipfs@$PWD/packages/ipfs,ipfs-core-types@$PWD/packages/ipfs-core-types,ipfs-http-client@$PWD/packages/ipfs-http-client
# - name: ipfs browser ipns publish
# repo: https://github.com/ipfs-examples/js-ipfs-browser-ipns-publish.git
# deps: ipfs-core@$PWD/packages/ipfs-core,ipfs-http-client@$PWD/packages/ipfs-http-client
# - name: ipfs browser mfs
# repo: https://github.com/ipfs-examples/js-ipfs-browser-mfs.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# # fails with No native build was found for platform=darwin arch=x64 runtime=node abi=93 uv=1 libc=glibc node=16.13.0 webpack=true
# #- name: ipfs browser nextjs
# # repo: https://github.com/ipfs-examples/js-ipfs-browser-nextjs.git
# # deps: ipfs-core@$PWD/packages/ipfs-core
# - name: ipfs browser parceljs
# repo: https://github.com/ipfs-examples/js-ipfs-browser-parceljs.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# - name: ipfs browser readable stream
# repo: https://github.com/ipfs-examples/js-ipfs-browser-readablestream.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# - name: ipfs browser service worker
# repo: https://github.com/ipfs-examples/js-ipfs-browser-service-worker.git
# deps: ipfs-core@$PWD/packages/ipfs-core,ipfs-message-port-client@$PWD/packages/ipfs-message-port-client,ipfs-message-port-protocol@$PWD/packages/ipfs-message-port-protocol,ipfs-message-port-server@$PWD/packages/ipfs-message-port-server
# - name: ipfs browser sharing across tabs
# repo: https://github.com/ipfs-examples/js-ipfs-browser-sharing-node-across-tabs.git
# deps: ipfs-core@$PWD/packages/ipfs-core,ipfs-message-port-client@$PWD/packages/ipfs-message-port-client,ipfs-message-port-server@$PWD/packages/ipfs-message-port-server
# - name: ipfs browser video streaming
# repo: https://github.com/ipfs-examples/js-ipfs-browser-video-streaming.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# - name: ipfs browser vue
# repo: https://github.com/ipfs-examples/js-ipfs-browser-vue.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# - name: ipfs browser webpack
# repo: https://github.com/ipfs-examples/js-ipfs-browser-webpack.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# - name: ipfs circuit relaying
# repo: https://github.com/ipfs-examples/js-ipfs-circuit-relaying.git
# deps: ipfs-core@$PWD/packages/ipfs-core,ipfs-http-client@$PWD/packages/ipfs-http-client
# - name: ipfs custom ipfs repo
# repo: https://github.com/ipfs-examples/js-ipfs-custom-ipfs-repo.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# - name: ipfs custom ipld formats
# repo: https://github.com/ipfs-examples/js-ipfs-custom-ipld-formats.git
# deps: ipfs-core@$PWD/packages/ipfs-core,ipfs-daemon@$PWD/packages/ipfs-daemon,ipfs-http-client@$PWD/packages/ipfs-http-client
# - name: ipfs custom libp2p
# repo: https://github.com/ipfs-examples/js-ipfs-custom-libp2p.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# - name: ipfs-http-client browser pubsub
# repo: https://github.com/ipfs-examples/js-ipfs-http-client-browser-pubsub.git
# deps: ipfs-http-client@$PWD/packages/ipfs-http-client,ipfs@$PWD/packages/ipfs
# - name: ipfs-http-client bundle webpack
# repo: https://github.com/ipfs-examples/js-ipfs-http-client-bundle-webpack.git
# deps: ipfs-http-client@$PWD/packages/ipfs-http-client,ipfs@$PWD/packages/ipfs
# - name: ipfs-http-client name api
# repo: https://github.com/ipfs-examples/js-ipfs-http-client-name-api.git
# deps: ipfs-http-client@$PWD/packages/ipfs-http-client
# - name: ipfs-http-client upload file
# repo: https://github.com/ipfs-examples/js-ipfs-http-client-upload-file.git
# deps: ipfs@$PWD/packages/ipfs,ipfs-http-client@$PWD/packages/ipfs-http-client
# - name: ipfs 101
# repo: https://github.com/ipfs-examples/js-ipfs-101.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# - name: ipfs-client add files
# repo: https://github.com/ipfs-examples/js-ipfs-ipfs-client-add-files.git
# deps: ipfs@$PWD/packages/ipfs,ipfs-client@$PWD/packages/ipfs-client
# - name: ipfs electron js
# repo: https://github.com/ipfs-examples/js-ipfs-run-in-electron.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# - name: ipfs running multiple nodes
# repo: https://github.com/ipfs-examples/js-ipfs-running-multiple-nodes.git
# deps: ipfs@$PWD/packages/ipfs
# - name: ipfs traverse ipld graphs
# repo: https://github.com/ipfs-examples/js-ipfs-traverse-ipld-graphs.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# - name: types with typescript
# repo: https://github.com/ipfs-examples/js-ipfs-types-use-ipfs-from-ts.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# - name: types with typed js
# repo: https://github.com/ipfs-examples/js-ipfs-types-use-ipfs-from-typed-js.git
# deps: ipfs-core@$PWD/packages/ipfs-core
# steps:
# - uses: actions/checkout@v2
# - uses: actions/setup-node@v2
# with:
# node-version: lts/*
# - uses: ipfs/aegir/actions/cache-node-modules@master
# - uses: GabrielBB/xvfb-action@v1
# name: Run npm run test:external -- -- -- ${{ matrix.example.repo }} --deps ${{ matrix.example.deps }}
# with:
# run: npm run test:external -- -- -- ${{ matrix.example.repo }} --deps ${{ matrix.example.deps }}
================================================
FILE: .github/workflows/externals.yml
================================================
name: Externals
on:
push:
branches:
- master
pull_request:
branches:
- '**'
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master
test-externals:
name: Test external ${{ matrix.external.name }}
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
external:
- name: ipfs webui
repo: https://github.com/ipfs-shipyard/ipfs-webui.git
deps: ipfs@$PWD/packages/ipfs
- name: ipfs companion
repo: https://github.com/ipfs-shipyard/ipfs-companion.git
deps: ipfs@$PWD/packages/ipfs
- name: orbit-db-io
repo: https://github.com/orbitdb/orbit-db-io.git
deps: ipfs@$PWD/packages/ipfs
- name: ipfs-log
repo: https://github.com/orbitdb/ipfs-log.git
deps: ipfs@$PWD/packages/ipfs,orbit-db-io@next
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master
- uses: GabrielBB/xvfb-action@v1
name: Run npm run test:external -- -- -- ${{ matrix.external.repo }} --deps ${{ matrix.external.deps }} --branch ${{ matrix.external.branch }}
continue-on-error: true
with:
run: npm run test:external -- -- -- ${{ matrix.external.repo }} --deps ${{ matrix.external.deps }} --branch ${{ matrix.external.branch }}
================================================
FILE: .github/workflows/stale.yml
================================================
name: Close and mark stale issue
on:
schedule:
- cron: '0 0 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Oops, seems like we needed more information for this issue, please comment with more details or this issue will be closed in 7 days.'
close-issue-message: 'This issue was closed because it is missing author input.'
stale-issue-label: 'kind/stale'
any-of-labels: 'need/author-input'
exempt-issue-labels: 'need/triage,need/community-input,need/maintainer-input,need/maintainers-input,need/analysis,status/blocked,status/in-progress,status/ready,status/deferred,status/inactive'
days-before-issue-stale: 6
days-before-issue-close: 7
enable-statistics: true
================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on:
push:
branches:
- master
pull_request:
branches:
- '**'
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master
check:
name: Check
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master
- run: |
npm run lint
npm run dep-check -- -- -- -p
npm run dep-check -- -- -- -- --unused
test-node:
name: Unit tests node ${{ matrix.node }} ${{ matrix.os }}
needs: build
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node: [lts/*]
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- uses: ipfs/aegir/actions/cache-node-modules@master
- run: npm run test:node
- uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0
with:
flags: node
test-chrome:
name: Unit tests chrome
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master
- run: npm run test:chrome
- uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0
with:
flags: chrome
test-chrome-webworker:
name: Unit tests chrome-webworker
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master
- run: npm run test:chrome-webworker
- uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0
with:
flags: chrome-webworker
test-firefox:
name: Unit tests firefox
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master
- run: npm run test:firefox
- uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0
with:
flags: firefox
test-firefox-webworker:
name: Unit tests firefox-webworker
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master
- run: npx playwright install --with-deps
- run: npm run test:firefox-webworker
- uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0
with:
flags: firefox-webworker
test-electron-main:
name: Unit tests electron-main
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master
- uses: GabrielBB/xvfb-action@v1
with:
run: npm run test:electron-main
- uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0
with:
flags: electron-main
test-interop:
name: Interop tests ${{ matrix.project }} ${{ matrix.type }}
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
type:
- node
- browser
#- electron-main
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master
- run: npm run test:interop -- -- -- -t ${{ matrix.type }}
- uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0
with:
flags: interop-${{ matrix.type }}
test-interface:
name: Interface tests ${{ matrix.suite }} ${{ matrix.type }}
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
type:
- node
- browser
#- electron-main
suite:
- test:interface:core
- test:interface:client
- test:interface:http-go
- test:interface:http-js
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master
- run: npm run ${{ matrix.suite }} -- -- -t ${{ matrix.type }}
- uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0
with:
flags: interface-${{ matrix.type }}
test-interface-message-port-client:
name: Interface tests test:interface:message-port-client browser
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: lts/*
- uses: ipfs/aegir/actions/cache-node-modules@master
- run: npx playwright install --with-deps
- run: npm run test:interface:message-port-client
release:
runs-on: ubuntu-latest
needs: [
test-node,
test-chrome,
test-chrome-webworker,
test-firefox,
test-firefox-webworker,
test-electron-main,
test-interop,
test-interface,
test-interface-message-port-client
]
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
steps:
- uses: GoogleCloudPlatform/release-please-action@v2
id: release
with:
token: ${{ secrets.GITHUB_TOKEN }}
command: manifest
release-type: node
manifest-file: .release-please-manifest.json
config-file: .release-please.json
changelog-types: |
[
{ "type": "feat", "section": "Features", "hidden": false },
{ "type": "fix", "section": "Bug Fixes", "hidden": false },
{ "type": "chore", "section": "Trivial Changes", "hidden": false },
{ "type": "docs", "section": "Documentation", "hidden": false },
{ "type": "deps", "section": "Dependencies", "hidden": false }
]
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: lts/*
registry-url: 'https://registry.npmjs.org'
- uses: ipfs/aegir/actions/cache-node-modules@master
- uses: ipfs/aegir/actions/docker-login@master
with:
docker-token: ${{ secrets.DOCKER_TOKEN }}
docker-username: ${{ secrets.DOCKER_USERNAME }}
- if: ${{ steps.release.outputs.releases_created }}
name: Run release version
run: |
git update-index --assume-unchanged packages/ipfs-core/src/version.js packages/ipfs-http-server/src/version.js packages/ipfs/src/package.js
npm run --if-present release
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- if: ${{ !steps.release.outputs.releases_created }}
name: Run release rc
run: |
git update-index --assume-unchanged packages/ipfs-core/src/version.js packages/ipfs-http-server/src/version.js packages/ipfs/src/package.js
npm run --if-present release:rc
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
package-lock.json
yarn.lock
tsconfig-types.aegir.json
# Coverage directory used by tools like istanbul
coverage
.coverage
.nyc_output
tests_output
cache
.cache
.parcel-cache
# Dependency directory
node_modules
# Build artefacts
dist
build
bundle.js
tsconfig-types.aegir.json
tsconfig-check.aegir.json
.tsbuildinfo
# Deployment files
.npmrc
# Editor files
.vscode
# Operating system files
.DS_Store
types
================================================
FILE: .release-please-manifest.json
================================================
{"packages/interface-ipfs-core":"0.158.1","packages/ipfs":"0.66.1","packages/ipfs-cli":"0.16.1","packages/ipfs-client":"0.10.1","packages/ipfs-core":"0.18.1","packages/ipfs-core-config":"0.7.1","packages/ipfs-core-types":"0.14.1","packages/ipfs-core-utils":"0.18.1","packages/ipfs-daemon":"0.16.1","packages/ipfs-grpc-client":"0.13.1","packages/ipfs-grpc-protocol":"0.8.1","packages/ipfs-grpc-server":"0.12.1","packages/ipfs-http-client":"60.0.1","packages/ipfs-http-gateway":"0.13.1","packages/ipfs-http-response":"6.0.1","packages/ipfs-http-server":"0.15.1","packages/ipfs-message-port-client":"0.15.1","packages/ipfs-message-port-protocol":"0.15.1","packages/ipfs-message-port-server":"0.15.1"}
================================================
FILE: .release-please.json
================================================
{
"plugins": ["node-workspace"],
"bump-minor-pre-major": true,
"group-pull-request-title-pattern": "chore: release ${component}",
"packages": {
"packages/interface-ipfs-core": {},
"packages/ipfs": {},
"packages/ipfs-cli": {},
"packages/ipfs-client": {},
"packages/ipfs-core": {},
"packages/ipfs-core-config": {},
"packages/ipfs-core-types": {},
"packages/ipfs-core-utils": {},
"packages/ipfs-daemon": {},
"packages/ipfs-grpc-client": {},
"packages/ipfs-grpc-protocol": {},
"packages/ipfs-grpc-server": {},
"packages/ipfs-http-client": {},
"packages/ipfs-http-gateway": {},
"packages/ipfs-http-response": {},
"packages/ipfs-http-server": {},
"packages/ipfs-message-port-client": {},
"packages/ipfs-message-port-protocol": {},
"packages/ipfs-message-port-server": {}
}
}
================================================
FILE: CHANGELOG.md
================================================
# Change Log
Please see the individual package changelogs for what's new:
* [`/packages/interface-ipfs-core/CHANGELOG.md`](./packages/interface-ipfs-core/CHANGELOG.md)
* [`/packages/ipfs/CHANGELOG.md`](./packages/ipfs/CHANGELOG.md)
* [`/packages/ipfs-core-utils/CHANGELOG.md`](./packages/ipfs-core-utils/CHANGELOG.md)
* [`/packages/ipfs-http-client/CHANGELOG.md`](./packages/ipfs-http-client/CHANGELOG.md)
* [`/packages/ipfs-http-server/CHANGELOG.md`](./packages/ipfs-http-server/CHANGELOG.md)
* [`/packages/ipfs-message-port-client/CHANGELOG.md`](./packages/ipfs-message-port-client/CHANGELOG.md)
* [`/packages/ipfs-message-port-protocol/CHANGELOG.md`](./packages/ipfs-message-port-protocol/CHANGELOG.md)
* [`/packages/ipfs-message-port-server/CHANGELOG.md`](./packages/ipfs-message-port-server/CHANGELOG.md)
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Code of Conduct
The `js-ipfs` project follows the [`IPFS Community Code of Conduct`](https://github.com/ipfs/community/blob/master/code-of-conduct.md)
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing guidelines
IPFS as a project, including js-ipfs and all of its modules, follows the [standard IPFS Community contributing guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md).
We also adhere to the [IPFS JavaScript Community contributing guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) which provide additional information of how to collaborate and contribute in the JavaScript implementation of IPFS.
We appreciate your time and attention for going over these. Please open an issue on [ipfs/community](https://github.com/ipfs/community) if you have any question.
Thank you.
================================================
FILE: COPYRIGHT
================================================
This project is transitioning from an MIT-only license to a dual MIT/Apache-2.0 license.
Unless otherwise noted, all code contributed prior to 2019-11-21 and not contributed by
a user listed in [this signoff issue](https://github.com/ipfs/js-ipfs/issues/2624) is
licensed under MIT-only. All new contributions (and past contributions since 2019-11-21)
are licensed under a dual MIT/Apache-2.0 license.
================================================
FILE: Dockerfile.latest
================================================
FROM node:16-alpine
ENV IPFS_VERSION=latest
ENV IPFS_MONITORING=1
ENV IPFS_PATH=/root/.jsipfs
ENV BUILD_DEPS='libnspr4 libnspr4-dev libnss3'
RUN apk add --no-cache git python3 build-base
# Hopefully remove when https://github.com/node-webrtc/node-webrtc/pull/694 is merged
RUN npm install -g ipfs@"$IPFS_VERSION"
# Make the image a bit smaller
RUN npm cache clear --force
RUN apk del build-base python3 git
# Configure jsipfs
RUN jsipfs init
RUN jsipfs version
# Allow connections from any host
RUN sed -i.bak "s/127.0.0.1/0.0.0.0/g" $IPFS_PATH/config
EXPOSE 4002
EXPOSE 4003
EXPOSE 5002
EXPOSE 9090
CMD jsipfs daemon
================================================
FILE: Dockerfile.next
================================================
FROM node:16-alpine
ENV IPFS_VERSION=next
ENV IPFS_MONITORING=1
ENV IPFS_PATH=/root/.jsipfs
ENV BUILD_DEPS='libnspr4 libnspr4-dev libnss3'
RUN apk add --no-cache git python3 build-base
# Hopefully remove when https://github.com/node-webrtc/node-webrtc/pull/694 is merged
RUN npm install -g ipfs@"$IPFS_VERSION"
# Make the image a bit smaller
RUN npm cache clear --force
RUN apk del build-base python3 git
# Configure jsipfs
RUN jsipfs init
RUN jsipfs version
# Allow connections from any host
RUN sed -i.bak "s/127.0.0.1/0.0.0.0/g" $IPFS_PATH/config
EXPOSE 4002
EXPOSE 4003
EXPOSE 5002
EXPOSE 9090
CMD jsipfs daemon
================================================
FILE: LICENSE
================================================
This project is dual licensed under MIT and Apache-2.0.
MIT: https://www.opensource.org/licenses/mit
Apache-2.0: https://www.apache.org/licenses/license-2.0
================================================
FILE: LICENSE-APACHE
================================================
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
================================================
FILE: LICENSE-MIT
================================================
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
> # ⛔️ DEPRECATED: [js-IPFS](https://github.com/ipfs/js-ipfs) has been superseded by [Helia](https://github.com/ipfs/helia)
>
> 📚 [Learn more about this deprecation](https://github.com/ipfs/js-ipfs/issues/4336) or [how to migrate](https://github.com/ipfs/helia/wiki/Migrating-from-js-IPFS)
>
> ⚠️ If you continue using this repo, please note that security fixes will not be provided
The JavaScript implementation of the IPFS protocol
## Getting started
* Read the [docs](https://github.com/ipfs/js-ipfs/tree/master/docs)
* Ensure CORS is [correctly configured](https://github.com/ipfs/js-ipfs/blob/master/docs/CORS.md) for use with the HTTP client
* Look into the [examples](https://github.com/ipfs-examples/js-ipfs-examples/tree/master) to learn how to spawn an IPFS node in Node.js and in the Browser
* Consult the [Core API docs](https://github.com/ipfs/js-ipfs/tree/master/docs/core-api) to see what you can do with an IPFS node
* Head over to https://proto.school to take the [IPFS course](https://proto.school/course/ipfs) that covers core IPFS concepts and JS APIs
* Check out https://docs.ipfs.io for [glossary](https://docs.ipfs.io/concepts/glossary), tips, how-tos and more
* Need help? Please ask 'How do I?' questions on https://discuss.ipfs.io
* Find out about chat channels, the IPFS newsletter, the IPFS blog, and more in the [IPFS community space](https://docs.ipfs.io/community/).
## Table of Contents
- [Getting started](#getting-started)
- [Install as a CLI user](#install-as-a-cli-user)
- [Install as an application developer](#install-as-an-application-developer)
- [Documentation](#documentation)
- [Structure](#structure)
- [Packages](#packages)
- [Want to hack on IPFS?](#want-to-hack-on-ipfs)
- [License](#license)
## Getting Started
### Install as a CLI user
Installing `ipfs` globally will give you the `jsipfs` command which you can use to start a daemon running:
```console
$ npm install -g ipfs
$ jsipfs daemon
Initializing IPFS daemon...
js-ipfs version: x.x.x
System version: x64/darwin
Node.js version: x.x.x
Swarm listening on /ip4/127.0
.... more output
```
You can then add a file:
```console
$ jsipfs add ./hello-world.txt
added QmXXY5ZxbtuYj6DnfApLiGstzPN7fvSyigrRee3hDWPCaf hello-world.txt
```
### Install as an application developer
If you do not need to run a command line daemon, use the `ipfs-core` package - it has all the features of `ipfs` but in a lighter package:
```console
$ npm install ipfs-core
```
Then start a node in your app:
```javascript
import * as IPFS from 'ipfs-core'
const ipfs = await IPFS.create()
const { cid } = await ipfs.add('Hello world')
console.info(cid)
// QmXXY5ZxbtuYj6DnfApLiGstzPN7fvSyigrRee3hDWPCaf
```
## Documentation
* [Concepts](https://docs.ipfs.io/concepts/)
* [Config](./docs/CONFIG.md)
* [Core API](./docs/core-api)
* [Examples](https://github.com/ipfs-examples/js-ipfs-examples/tree/master/examples)
* [Development](./docs/DEVELOPMENT.md)
## Structure
This project is broken into several modules, their purposes are:
* [`/packages/interface-ipfs-core`](./packages/interface-ipfs-core) Tests to ensure adherence of an implementation to the spec
* [`/packages/ipfs`](./packages/ipfs) An aggregator module that bundles the core implementation, the CLI, HTTP API server and daemon
* [`/packages/ipfs-cli`](./packages/ipfs-cli) A CLI to the core implementation
* [`/packages/ipfs-core`](./packages/ipfs-core) The core implementation
* [`/packages/ipfs-core-types`](./packages/ipfs-core-types) Typescript definitions for the core API
* [`/packages/ipfs-core-utils`](./packages/ipfs-core-utils) Helpers and utilities common to core and the HTTP RPC API client
* [`/packages/ipfs-daemon`](./packages/ipfs-daemon) Run js-IPFS as a background daemon
* [`/packages/ipfs-grpc-client`](./packages/ipfs-grpc-client) A gRPC client for js-IPFS
* [`/packages/ipfs-grpc-protocol`](./packages/ipfs-grpc-protocol) Shared module between the gRPC client and server
* [`/packages/ipfs-grpc-server`](./packages/ipfs-grpc-server) A gRPC-over-websockets server for js-IPFS
* [`/packages/ipfs-http-client`](./packages/ipfs-http-client) A client for the RPC-over-HTTP API presented by both js-ipfs and go-ipfs
* [`/packages/ipfs-http-server`](./packages/ipfs-http-server) JS implementation of the [Kubo RPC HTTP API](https://docs.ipfs.io/reference/kubo/rpc/)
* [`/packages/ipfs-http-gateway`](./packages/ipfs-http-gateway) JS implementation of the [IPFS HTTP Gateway](https://docs.ipfs.io/concepts/ipfs-gateway/)
* [`/packages/ipfs-http-response`](./packages/ipfs-http-response) Creates a HTTP response for a given IPFS Path
* [`/packages/ipfs-message-port-client`](./packages/ipfs-message-port-client) A client for the RPC-over-message-port API presented by js-ipfs running in a shared worker
* [`/packages/ipfs-message-port-protocol`](./packages/ipfs-message-port-protocol) Code shared by the message port client & server
* [`/packages/ipfs-message-port-server`](./packages/ipfs-message-port-server) The server that receives requests from ipfs-message-port-client
## Packages
List of the main packages that make up the IPFS ecosystem.
| Package | Version | Deps | CI/Travis | Coverage | Lead Maintainer |
| ---------|---------|---------|---------|---------|--------- |
| **Files** |
| [`ipfs-unixfs`](//github.com/ipfs/js-ipfs-unixfs) | [](//github.com/ipfs/js-ipfs-unixfs/releases) | [](https://david-dm.org/ipfs/js-ipfs-unixfs) | [](https://travis-ci.com/ipfs/js-ipfs-unixfs) | [](https://codecov.io/gh/ipfs/js-ipfs-unixfs) | [Alex Potsides](mailto:alex.potsides@protocol.ai) |
| **Repo** |
| [`ipfs-repo`](//github.com/ipfs/js-ipfs-repo) | [](//github.com/ipfs/js-ipfs-repo/releases) | [](https://david-dm.org/ipfs/js-ipfs-repo) | [](https://travis-ci.com/ipfs/js-ipfs-repo) | [](https://codecov.io/gh/ipfs/js-ipfs-repo) | [Alex Potsides](mailto:alex@achingbrain.net) |
| [`ipfs-repo-migrations`](//github.com/ipfs/js-ipfs-repo-migrations) | [](//github.com/ipfs/js-ipfs-repo-migrations/releases) | [](https://david-dm.org/ipfs/js-ipfs-repo-migrations) | [](https://travis-ci.com/ipfs/js-ipfs-repo-migrations) | [](https://codecov.io/gh/ipfs/js-ipfs-repo-migrations) | N/A |
| **Exchange** |
| [`ipfs-bitswap`](//github.com/ipfs/js-ipfs-bitswap) | [](//github.com/ipfs/js-ipfs-bitswap/releases) | [](https://david-dm.org/ipfs/js-ipfs-bitswap) | [](https://travis-ci.com/ipfs/js-ipfs-bitswap) | [](https://codecov.io/gh/ipfs/js-ipfs-bitswap) | [Dirk McCormick](mailto:dirk@protocol.ai) |
| **IPNS** |
| [`ipns`](//github.com/ipfs/js-ipns) | [](//github.com/ipfs/js-ipns/releases) | [](https://david-dm.org/ipfs/js-ipns) | [](https://travis-ci.com/ipfs/js-ipns) | [](https://codecov.io/gh/ipfs/js-ipns) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
| **Generics/Utils** |
| [`ipfs-utils`](//github.com/ipfs/js-ipfs) | [](//github.com/ipfs/js-ipfs/releases) | [](https://david-dm.org/ipfs/js-ipfs) | [](https://travis-ci.com/ipfs/js-ipfs) | [](https://codecov.io/gh/ipfs/js-ipfs) | [Hugo Dias](mailto:hugomrdias@gmail.com) |
| [`ipfs-http-client`](//github.com/ipfs/js-ipfs) | [](//github.com/ipfs/js-ipfs/releases) | [](https://david-dm.org/ipfs/js-ipfs) | [](https://travis-ci.com/ipfs/js-ipfs) | [](https://codecov.io/gh/ipfs/js-ipfs) | [Alex Potsides](mailto:alex@achingbrain.net) |
| [`ipfs-http-response`](//github.com/ipfs/js-ipfs-http-response) | [](//github.com/ipfs/js-ipfs-http-response/releases) | [](https://david-dm.org/ipfs/js-ipfs-http-response) | [](https://travis-ci.com/ipfs/js-ipfs-http-response) | [](https://codecov.io/gh/ipfs/js-ipfs-http-response) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
| [`ipfsd-ctl`](//github.com/ipfs/js-ipfsd-ctl) | [](//github.com/ipfs/js-ipfsd-ctl/releases) | [](https://david-dm.org/ipfs/js-ipfsd-ctl) | [](https://travis-ci.com/ipfs/js-ipfsd-ctl) | [](https://codecov.io/gh/ipfs/js-ipfsd-ctl) | [Hugo Dias](mailto:mail@hugodias.me) |
| [`is-ipfs`](//github.com/ipfs/is-ipfs) | [](//github.com/ipfs/is-ipfs/releases) | [](https://david-dm.org/ipfs/is-ipfs) | [](https://travis-ci.com/ipfs/is-ipfs) | [](https://codecov.io/gh/ipfs/is-ipfs) | [Marcin Rataj](mailto:lidel@lidel.org) |
| [`aegir`](//github.com/ipfs/aegir) | [](//github.com/ipfs/aegir/releases) | [](https://david-dm.org/ipfs/aegir) | [](https://travis-ci.com/ipfs/aegir) | [](https://codecov.io/gh/ipfs/aegir) | [Hugo Dias](mailto:hugomrdias@gmail.com) |
| **libp2p** |
| [`libp2p`](//github.com/libp2p/js-libp2p) | [](//github.com/libp2p/js-libp2p/releases) | [](https://david-dm.org/libp2p/js-libp2p) | [](https://travis-ci.com/libp2p/js-libp2p) | [](https://codecov.io/gh/libp2p/js-libp2p) | [Jacob Heun](mailto:jacobheun@gmail.com) |
| [`peer-id`](//github.com/libp2p/js-peer-id) | [](//github.com/libp2p/js-peer-id/releases) | [](https://david-dm.org/libp2p/js-peer-id) | [](https://travis-ci.com/libp2p/js-peer-id) | [](https://codecov.io/gh/libp2p/js-peer-id) | [Vasco Santos](mailto:santos.vasco10@gmail.com) |
| [`libp2p-crypto`](//github.com/libp2p/js-libp2p-crypto) | [](//github.com/libp2p/js-libp2p-crypto/releases) | [](https://david-dm.org/libp2p/js-libp2p-crypto) | [](https://travis-ci.com/libp2p/js-libp2p-crypto) | [](https://codecov.io/gh/libp2p/js-libp2p-crypto) | [Jacob Heun](mailto:jacobheun@gmail.com) |
| [`libp2p-floodsub`](//github.com/libp2p/js-libp2p-floodsub) | [](//github.com/libp2p/js-libp2p-floodsub/releases) | [](https://david-dm.org/libp2p/js-libp2p-floodsub) | [](https://travis-ci.com/libp2p/js-libp2p-floodsub) | [](https://codecov.io/gh/libp2p/js-libp2p-floodsub) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
| [`libp2p-gossipsub`](//github.com/ChainSafe/gossipsub-js) | [](//github.com/ChainSafe/gossipsub-js/releases) | [](https://david-dm.org/ChainSafe/gossipsub-js) | [](https://travis-ci.com/ChainSafe/gossipsub-js) | [](https://codecov.io/gh/ChainSafe/gossipsub-js) | [Cayman Nava](mailto:caymannava@gmail.com) |
| [`libp2p-kad-dht`](//github.com/libp2p/js-libp2p-kad-dht) | [](//github.com/libp2p/js-libp2p-kad-dht/releases) | [](https://david-dm.org/libp2p/js-libp2p-kad-dht) | [](https://travis-ci.com/libp2p/js-libp2p-kad-dht) | [](https://codecov.io/gh/libp2p/js-libp2p-kad-dht) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
| [`libp2p-mdns`](//github.com/libp2p/js-libp2p-mdns) | [](//github.com/libp2p/js-libp2p-mdns/releases) | [](https://david-dm.org/libp2p/js-libp2p-mdns) | [](https://travis-ci.com/libp2p/js-libp2p-mdns) | [](https://codecov.io/gh/libp2p/js-libp2p-mdns) | [Jacob Heun](mailto:jacobheun@gmail.com) |
| [`libp2p-bootstrap`](//github.com/libp2p/js-libp2p-bootstrap) | [](//github.com/libp2p/js-libp2p-bootstrap/releases) | [](https://david-dm.org/libp2p/js-libp2p-bootstrap) | [](https://travis-ci.com/libp2p/js-libp2p-bootstrap) | [](https://codecov.io/gh/libp2p/js-libp2p-bootstrap) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
| [`@chainsafe/libp2p-noise`](//github.com/ChainSafe/js-libp2p-noise) | [](//github.com/ChainSafe/js-libp2p-noise/releases) | [](https://david-dm.org/ChainSafe/js-libp2p-noise) | [](https://travis-ci.com/ChainSafe/js-libp2p-noise) | [](https://codecov.io/gh/ChainSafe/js-libp2p-noise) | N/A |
| [`libp2p-tcp`](//github.com/libp2p/js-libp2p-tcp) | [](//github.com/libp2p/js-libp2p-tcp/releases) | [](https://david-dm.org/libp2p/js-libp2p-tcp) | [](https://travis-ci.com/libp2p/js-libp2p-tcp) | [](https://codecov.io/gh/libp2p/js-libp2p-tcp) | [Jacob Heun](mailto:jacobheun@gmail.com) |
| [`libp2p-webrtc-star`](//github.com/libp2p/js-libp2p-webrtc-star) | [](//github.com/libp2p/js-libp2p-webrtc-star/releases) | [](https://david-dm.org/libp2p/js-libp2p-webrtc-star) | [](https://travis-ci.com/libp2p/js-libp2p-webrtc-star) | [](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
| [`libp2p-websockets`](//github.com/libp2p/js-libp2p-websockets) | [](//github.com/libp2p/js-libp2p-websockets/releases) | [](https://david-dm.org/libp2p/js-libp2p-websockets) | [](https://travis-ci.com/libp2p/js-libp2p-websockets) | [](https://codecov.io/gh/libp2p/js-libp2p-websockets) | [Jacob Heun](mailto:jacobheun@gmail.com) |
| [`libp2p-mplex`](//github.com/libp2p/js-libp2p-mplex) | [](//github.com/libp2p/js-libp2p-mplex/releases) | [](https://david-dm.org/libp2p/js-libp2p-mplex) | [](https://travis-ci.com/libp2p/js-libp2p-mplex) | [](https://codecov.io/gh/libp2p/js-libp2p-mplex) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
| [`libp2p-delegated-content-routing`](//github.com/libp2p/js-libp2p-delegated-content-routing) | [](//github.com/libp2p/js-libp2p-delegated-content-routing/releases) | [](https://david-dm.org/libp2p/js-libp2p-delegated-content-routing) | [](https://travis-ci.com/libp2p/js-libp2p-delegated-content-routing) | [](https://codecov.io/gh/libp2p/js-libp2p-delegated-content-routing) | [Jacob Heun](mailto:jacobheun@gmail.com) |
| [`libp2p-delegated-peer-routing`](//github.com/libp2p/js-libp2p-delegated-peer-routing) | [](//github.com/libp2p/js-libp2p-delegated-peer-routing/releases) | [](https://david-dm.org/libp2p/js-libp2p-delegated-peer-routing) | [](https://travis-ci.com/libp2p/js-libp2p-delegated-peer-routing) | [](https://codecov.io/gh/libp2p/js-libp2p-delegated-peer-routing) | [Jacob Heun](mailto:jacobheun@gmail.com) |
| **IPLD** |
| [`@ipld/dag-pb`](//github.com/ipld/js-dag-pb) | [](//github.com/ipld/js-dag-pb/releases) | [](https://david-dm.org/ipld/js-dag-pb) | [](https://travis-ci.com/ipld/js-dag-pb) | [](https://codecov.io/gh/ipld/js-dag-pb) | N/A |
| [`@ipld/dag-cbor`](//github.com/ipld/js-dag-cbor) | [](//github.com/ipld/js-dag-cbor/releases) | [](https://david-dm.org/ipld/js-dag-cbor) | [](https://travis-ci.com/ipld/js-dag-cbor) | [](https://codecov.io/gh/ipld/js-dag-cbor) | N/A |
| **Multiformats** |
| [`multiformats`](//github.com/multiformats/js-multiformats) | [](//github.com/multiformats/js-multiformats/releases) | [](https://david-dm.org/multiformats/js-multiformats) | [](https://travis-ci.com/multiformats/js-multiformats) | [](https://codecov.io/gh/multiformats/js-multiformats) | N/A |
| [`mafmt`](//github.com/multiformats/js-mafmt) | [](//github.com/multiformats/js-mafmt/releases) | [](https://david-dm.org/multiformats/js-mafmt) | [](https://travis-ci.com/multiformats/js-mafmt) | [](https://codecov.io/gh/multiformats/js-mafmt) | [Vasco Santos](mailto:vasco.santos@moxy.studio) |
| [`multiaddr`](//github.com/multiformats/js-multiaddr) | [](//github.com/multiformats/js-multiaddr/releases) | [](https://david-dm.org/multiformats/js-multiaddr) | [](https://travis-ci.com/multiformats/js-multiaddr) | [](https://codecov.io/gh/multiformats/js-multiaddr) | [Jacob Heun](mailto:jacobheun@gmail.com) |
> This table is generated using the module [`package-table`](https://www.npmjs.com/package/package-table) with `package-table --data=package-list.json`.
## Want to hack on IPFS?
[](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md)
The IPFS implementation in JavaScript needs your help! There are a few things you can do right now to help out:
Read the [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md) and [JavaScript Contributing Guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md).
- **Check out existing issues** The [issue list](https://github.com/ipfs/js-ipfs/issues) has many that are marked as ['help wanted'](https://github.com/ipfs/js-ipfs/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22help+wanted%22) or ['difficulty:easy'](https://github.com/ipfs/js-ipfs/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Adifficulty%3Aeasy) which make great starting points for development, many of which can be tackled with no prior IPFS knowledge
- **Look at the [IPFS Roadmap](https://github.com/ipfs/roadmap)** This are the high priority items being worked on right now
- **Perform code reviews** More eyes will help
a. speed the project along
b. ensure quality, and
c. reduce possible future bugs.
- **Add tests**. There can never be enough tests.
## License
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fipfs%2Fjs-ipfs?ref=badge_large)
================================================
FILE: docs/ARCHITECTURE.md
================================================
# IPFS Architecture
## Table of Contents
- [Code Architecture and folder Structure](#code-architecture-and-folder-structure)
- [Source code](#source-code)

[Annotated version](https://user-images.githubusercontent.com/1211152/47606420-b6265780-da13-11e8-923b-b365a8534e0e.png)
What does this image explain?
- IPFS uses `ipfs-repo` which picks `fs` or `indexeddb` as its storage drivers, depending if it is running in Node.js or in the Browser.
- The exchange protocol, `bitswap`, uses the Block Service which in turn uses the Repo, offering a get and put of blocks to the IPFS implementation.
- The DAG API (previously Object) comes from the IPLD Resolver, it can support several IPLD Formats (i.e: dag-pb, dag-cbor, etc).
- The Files API uses `ipfs-unixfs-engine` to import and export files to and from IPFS.
- libp2p, the network stack of IPFS, uses libp2p to dial and listen for connections, to use the DHT, for discovery mechanisms, and more.
## Code Architecture and folder Structure

### Source code
```Bash
> tree src -L 2
src # Main source code folder
├── cli # Implementation of the IPFS CLI
│ └── ...
├── http # The HTTP-API implementation of IPFS as defined by HTTP API spec
├── core # IPFS implementation, the core (what gets loaded in browser)
│ ├── components # Each of IPFS subcomponent
│ └── ...
└── ...
```
================================================
FILE: docs/BROWSERS.md
================================================
# Using JS IPFS in the Browser
## Table of Contents
- [Limitations of the Browser Context](#limitations-of-the-browser-context)
- [Addressing Limitations](#addressing-limitations)
- [Best Practices](#best-practices)
- [Code Examples](#code-examples)
JS IPFS is the implementation of IPFS protocol in JavaScript. It can run on any
evergreen browser, inside a service or web worker, browser extensions, Electron, and in Node.js.
**This document provides key information about running JS IPFS in the browser.
Save time and get familiar with common caveats and limitations of the browser context.**
## Limitations of the Browser Context
- Transport options are currently limited to [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) and [WebRTC](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API).
This means JS IPFS running in the browser is limited to Web APIs available on a web page.
There is no access to raw TCP sockets nor low-level UDP, only WebSockets, and WebRTC.
- Key [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API) require or are restricted by [Secure Context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) policies.
This means JS IPFS needs to run within Secure Context (HTTPS or localhost).
JS IPFS running on HTTPS website requires Secure WebSockets (TLS) and won't work with unencrypted ones.
[Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) not being available at all.
- JS IPFS comes with limited support for the [DHT](https://docs.ipfs.tech/concepts/dht/) in client mode which delegates content discovery requests to other DHT nodes.
However, it's worth noting that even though you'll get results from DHT queries, most nodes in the network are not dialable from browsers because they only support TCP and/or QUIC transports.
For now, the content discovery and connectivity to other peers are achieved with a mix of DHT client requests, rendezvous and relay servers, delegated peer/content routing, and preload servers.
## Addressing Limitations
We provide a few additional components useful for running JS IPFS in the browser:
- [libp2p-webrtc-star](https://github.com/libp2p/js-libp2p-webrtc-star) - incorporates both a transport and a discovery service that is facilitated by the custom rendezvous server available in the repo
- Instructions on enabling `webrtc-star` in js-ipfs config can be found [here](https://github.com/ipfs/js-ipfs/blob/master/docs/FAQ.md#how-to-enable-webrtc-support-for-js-ipfs-in-the-browser).
- Make sure to [run your own rendezvous server](https://github.com/libp2p/js-libp2p-webrtc-star#rendezvous-server-aka-signalling-server).
- [libp2p-webrtc-direct](https://github.com/libp2p/js-libp2p-webrtc-direct) - a WebRTC transport that doesn't require the set up a signaling server.
- Caveat: you can only establish Browser to Node.js and Node.js to Node.js connections.
**Note:** those are semi-centralized solutions. We are working towards replacing `*-star` with ambient relays and [libp2p-rendezvous](https://github.com/libp2p/js-libp2p-rendezvous). Details and progress can be found [here](https://github.com/libp2p/js-libp2p/issues/385).
You can find detailed information about running js-ipfs [here](https://github.com/ipfs/js-ipfs#table-of-contents).
## Best Practices
- Configure nodes for using self-hosted `*-star` signalling and transport service. When in doubt, use WebSockets ones.
- Run your own instance of `*-star` signalling service.
The default ones are under high load and should be used only for tests and development.
- Make sure content added to js-ipfs running in the browser is persisted/cached somewhere on a regular long-running IPFS daemon, e.g. [kubo](https://github.com/ipfs/kubo/)
- Manually `pin` or preload CIDs of interest with `refs -r` beforehand.
- Preload content on the fly using [preload](https://github.com/ipfs/js-ipfs/blob/master/docs/MODULE.md#optionspreload) feature and/or
configure [delegated routing](https://github.com/ipfs/js-ipfs/blob/master/docs/DELEGATE_ROUTERS.md).
- Avoid public instances in production environments. Make sure preload and delegate nodes used in config are self-hosted and under your control (expose a subset of [kubo](https://github.com/ipfs/kubo/) (formerly go-ipfs) APIs via reverse proxy such as Nginx).
- If your main goal is to provide content and files to the IPFS network from a browser and you would like to avoid running infrastructure, consider using a pinning service like [Web3.storage](https://web3.storage/).
## Code Examples
Prebuilt bundles are available, using JS IPFS in the browser is as simple as:
```js
```
More advanced examples and tutorials can be found in the [examples](https://github.com/ipfs-examples)
================================================
FILE: docs/CLI.md
================================================
# IPFS CLI
## Table of contents
- [Overview](#overview)
- [Configuration](#configuration)
## Overview
In order to use js-ipfs as a CLI, you must install it with the `global` flag. Run the following (even if you have ipfs installed locally):
```bash
npm install ipfs --global
```
The CLI is available by using the command `jsipfs` in your terminal. This is aliased, instead of using `ipfs`, to make sure it does not conflict with the [Go implementation](https://github.com/ipfs/go-ipfs).
Once installed, please follow the [Getting Started Guide](https://docs.ipfs.io/introduction/usage/) to learn how to initialize your node and run the daemon.
```sh
# Install js-ipfs globally
> jsipfs --help
Commands:
bitswap A set of commands to manipulate the bitswap agent.
block Manipulate raw IPFS blocks.
bootstrap Show or edit the list of bootstrap peers.
commands List all available commands
config [value] Get and set IPFS config values
daemon Start a long-running daemon process
# ...
```
## Configuration
`js-ipfs` uses some different default config values, so that they don't clash directly with a go-ipfs node running in the same machine. These are:
- default repo location: `~/.jsipfs` (can be changed with env variable `IPFS_PATH`)
- default swarm port: `4002`
- default API port: `5002`
================================================
FILE: docs/CONFIG.md
================================================
# The js-ipfs config file
The js-ipfs config file is a JSON document located in the root directory of the js-ipfs repository.
## Table of Contents
- [Profiles](#profiles)
- [`Addresses`](#addresses)
- [`API`](#api)
- [`RPC`](#rpc)
- [`Delegates`](#delegates)
- [`Gateway`](#gateway)
- [`Swarm`](#swarm)
- [`Announce`](#announce)
- [`Bootstrap`](#bootstrap)
- [`Datastore`](#datastore)
- [`Spec`](#spec)
- [`Discovery`](#discovery)
- [`MDNS`](#mdns)
- [`webRTCStar`](#webrtcstar)
- [`Identity`](#identity)
- [`PeerID`](#peerid)
- [`PrivKey`](#privkey)
- [`Keychain`](#keychain)
- [`Pubsub`](#pubsub)
- [`Router`](#router)
- [`Enabled`](#enabled)
- [`Swarm`](#swarm-1)
- [`ConnMgr`](#connmgr)
- [`DisableNatPortMap`](#disablenatportmap)
- [Example](#example)
- [`API`](#api-1)
- [`HTTPHeaders`](#httpheaders)
- [`Access-Control-Allow-Origin`](#access-control-allow-origin)
- [Example](#example-1)
- [`Access-Control-Allow-Credentials`](#access-control-allow-credentials)
- [Example](#example-2)
## Profiles
Configuration profiles allow to tweak configuration quickly. Profiles can be
applied with `--profile` flag to `ipfs init` or with the `ipfs config profile
apply` command. When a profile is applied a backup of the configuration file
will be created in `$IPFS_PATH`.
Available profiles:
- `server`
Recommended for nodes with public IPv4 address (servers, VPSes, etc.),
disables host and content discovery in local networks.
- `local-discovery`
Sets default values to fields affected by `server` profile, enables
discovery in local networks.
- `test`
Reduces external interference, useful for running ipfs in test environments.
Note that with these settings node won't be able to talk to the rest of the
network without manual bootstrap.
- `default-networking`
Restores default network settings. Inverse profile of the `test` profile.
- `lowpower`
Reduces daemon overhead on the system. May affect node functionality,
performance of content discovery and data fetching may be degraded.
- `default-power`
Inverse of "lowpower" profile.
## `Addresses`
Contains information about various listener addresses to be used by this node.
### `API`
The IPFS daemon exposes an HTTP API that allows to control the node and run the same commands as you can do from the command line. It is defined on the [HTTP API Spec](https://docs.ipfs.io/reference/api/http).
[Multiaddr](https://github.com/multiformats/multiaddr/) or array of [Multiaddr](https://github.com/multiformats/multiaddr/) describing the address(es) to serve the HTTP API on.
Default: `/ip4/127.0.0.1/tcp/5002`
### `RPC`
js-IPFS has a gRPC-over-websockets server that allows it to do things that you cannot do over HTTP like bi-directional streaming. It implements the same API as the [HTTP API Spec](https://docs.ipfs.io/reference/api/http) and can be accessed using the [ipfs-client](https://www.npmjs.com/package/ipfs-client) module.
Configure the address it listens on using this config key.
Default: `/ip4/127.0.0.1/tcp/5003`
### `Delegates`
Delegate peers are used to find peers and retrieve content from the network on your behalf.
Array of [Multiaddr](https://github.com/multiformats/multiaddr/) describing which addresses to use as delegate nodes.
Default: `[]`
### `Gateway`
A gateway is exposed by the IPFS daemon, which allows an easy way to access content from IPFS, using an IPFS path.
[Multiaddr](https://github.com/multiformats/multiaddr/) or array of [Multiaddr](https://github.com/multiformats/multiaddr/) describing the address(es) to serve the gateway on.
Default: `/ip4/127.0.0.1/tcp/9090`
### `Swarm`
Array of [Multiaddr](https://github.com/multiformats/multiaddr/) describing which addresses to listen on for p2p swarm connections.
Default:
```json
[
"/ip4/0.0.0.0/tcp/4002",
"/ip4/127.0.0.1/tcp/4003/ws"
]
```
### `Announce`
Array of [Multiaddr](https://github.com/multiformats/multiaddr/) describing which addresses to [announce](https://github.com/libp2p/js-libp2p/tree/master/src/address-manager#announce-addresses) over the network.
Default:
```json
[]
```
## `Bootstrap`
Bootstrap is an array of [Multiaddr](https://github.com/multiformats/multiaddr/) of trusted nodes to connect to in order to
initiate a connection to the network.
## `Datastore`
Contains information related to the construction and operation of the on-disk storage system.
### `Spec`
Spec defines the structure of the IPFS datastore. It is a composable structure, where each datastore is represented by a JSON object. Datastores can wrap other datastores to provide extra functionality (e.g. metrics, logging, or caching).
This can be changed manually, however, if you make any changes that require a different on-disk structure, you will need to run the [ipfs-ds-convert tool](https://github.com/ipfs/ipfs-ds-convert) to migrate data into the new structures.
Default:
```json
{
"mounts": [
{
"child": {
"path": "blocks",
"shardFunc": "/repo/flatfs/shard/v1/next-to-last/2",
"sync": true,
"type": "flatfs"
},
"mountpoint": "/blocks",
"prefix": "flatfs.datastore",
"type": "measure"
},
{
"child": {
"compression": "none",
"path": "datastore",
"type": "levelds"
},
"mountpoint": "/",
"prefix": "leveldb.datastore",
"type": "measure"
}
],
"type": "mount"
}
```
## `Discovery`
Contains options for configuring IPFS node discovery mechanisms.
### `MDNS`
Multicast DNS is a discovery protocol that is able to find other peers on the local network.
Options for Multicast DNS peer discovery:
- `Enabled`
A boolean value for whether or not MDNS should be active.
Default: `true`
- `Interval`
A number of seconds to wait between discovery checks.
Default: `10`
### `webRTCStar`
WebRTCStar is a discovery mechanism provided by a signalling-star that allows peer-to-peer communications in the browser.
Options for webRTCstar peer discovery:
- `Enabled`
A boolean value for whether or not webRTCStar should be active.
Default: `true`
## `Identity`
### `PeerID`
The unique PKI identity label for this configs peer. Set on init and never read, its merely here for convenience. IPFS will always generate the peerID from its keypair at runtime.
### `PrivKey`
The base64 encoded protobuf describing (and containing) the nodes private key.
## `Keychain`
We can customize the key management and cryptographically protected messages by changing the Keychain options. Those options are used for generating the derived encryption key (`DEK`). The `DEK` object, along with the passPhrase, is the input to a PBKDF2 function.
Default:
```json
{
"dek": {
"keyLength": 512/8,
"iterationCount": 1000,
"salt": "at least 16 characters long",
"hash": "sha2-512"
}
}
```
You can check the [parameter choice for pbkdf2](https://cryptosense.com/parameter-choice-for-pbkdf2/) for more information.
## `Pubsub`
Options for configuring the pubsub subsystem. It is important pointing out that this is not supported in the browser. If you want to configure a different pubsub router in the browser you must configure `libp2p.modules.pubsub` options instead.
### `Router`
A string value for specifying which pubsub routing protocol to use. You can either use `gossipsub` in order to use the [ChainSafe/gossipsub-js](https://github.com/ChainSafe/gossipsub-js) implementation, or `floodsub` to use the [libp2p/js-libp2p-floodsub](https://github.com/libp2p/js-libp2p-floodsub) implementation. You can read more about these implementations on the [libp2p/specs/pubsub](https://github.com/libp2p/specs/tree/master/pubsub) document.
Default: `gossipsub`
### `Enabled`
A boolean value for wether or not pubsub router should be active.
Default: `true`
## `Swarm`
Options for configuring the swarm.
### `ConnMgr`
The connection manager determines which and how many connections to keep and can be configured to keep.
- `LowWater`
The minimum number of connections to maintain.
Default: `200` (both browser and node.js)
- `HighWater`
The number of connections that, when exceeded, will trigger a connection GC operation.
Default: `500` (both browser and node.js)
The "basic" connection manager tries to keep between `LowWater` and `HighWater` connections. It works by:
1. Keeping all connections until `HighWater` connections is reached.
2. Once `HighWater` is reached, it closes connections until `LowWater` is reached.
### `DisableNatPortMap`
By default when running under nodejs, libp2p will try to use [UPnP](https://en.wikipedia.org/wiki/Universal_Plug_and_Play) to open a random high port on your router for any TCP connections you have configured.
Set `DisableNatPortMap` to `true` to disable this behaviour.
### Example
```json
{
"Swarm": {
"ConnMgr": {
"LowWater": 100,
"HighWater": 200,
}
},
"DisableNatPortMap": false
}
```
## `API`
Settings applied to the HTTP RPC API server
### `HTTPHeaders`
HTTP header settings used by the HTTP RPC API server
#### `Access-Control-Allow-Origin`
The RPC API endpoints running on your local node are protected by the [Cross-Origin Resource Sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) mechanism.
When a request is made that sends an `Origin` header, that Origin must be present in the allowed origins configured for the node, otherwise the browser will disallow that request to proceed, unless `mode: 'no-cors'` is set on the request, in which case the response will be opaque.
To allow requests from web browsers, configure the `API.HTTPHeaders.Access-Control-Allow-Origin` setting. This is an array of URL strings with safelisted Origins.
##### Example
If you are running a webapp locally that you access via the URL `http://127.0.0.1:3000`, you must add it to the list of allowed origins in order to make API requests from that webapp in the browser:
```json
{
"API": {
"HTTPHeaders": {
"Access-Control-Allow-Origin": [
"http://127.0.0.1:3000"
]
}
}
}
```
Note that the origin must match exactly so `'http://127.0.0.1:3000'` is treated differently to `'http://127.0.0.1:3000/'`
#### `Access-Control-Allow-Credentials`
The [Access-Control-Allow-Credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) header allows client-side JavaScript running in the browser to send and receive credentials with requests - cookies, auth headers or TLS certificates.
For most applications this will not be necessary but if you require this to be set, see the example below for how to configure it.
##### Example
```json
{
"API": {
"HTTPHeaders": {
"Access-Control-Allow-Credentials": true
}
}
}
```
================================================
FILE: docs/CORS.md
================================================
# CORS
## Table of Contents
- [Overview](#overview)
- [Configure CORS headers](#configure-cors-headers)
## Overview
Cross-origin Resource Sharing is a browser security mechanism that prevents unauthorized scripts from accessing resources from different domains.
By default the HTTP RPC API of js-IPFS will cause any request sent from a CORS-respecting browser to fail.
## Configure CORS headers
You can configure your node to allow requests from other domains to proceed by setting the appropriate headers in the node config:
```console
$ jsipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["http://example.com"]'
$ jsipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "POST", "GET"]'
```
Restart the daemon for the settings to take effect.
================================================
FILE: docs/DAEMON.md
================================================
# Running IPFS as a daemon
> How to run a long-lived IPFS process
## Table of contents
- [CLI](#cli)
- [Programmatic](#programmatic)
## CLI
To start a daemon on the CLI, use the `daemon` command:
```console
jsipfs daemon
```
The IPFS Daemon exposes the API defined in the [HTTP API spec](https://docs.ipfs.io/reference/api/http/). You can use any of the IPFS HTTP-API client libraries with it, such as: [ipfs-http-client](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs-http-client).
## Programmatic
If you want a programmatic way to spawn a IPFS Daemon using JavaScript, check out the [ipfsd-ctl](https://github.com/ipfs/js-ipfsd-ctl) module.
```javascript
import { createFactory } from 'ipfsd-ctl'
const factory = createFactory({
type: 'proc' // or 'js' to run in a separate process
})
const node = await factory.create()
// print the node ide
console.info(await node.id())
```
================================================
FILE: docs/DELEGATE_ROUTERS.md
================================================
# Configuring Delegate Routers
- [What is it?](#what-is-it)
- [How do I do it?](#how-do-i-do-it)
## What is it?
Delegate routers perform tasks on behalf of nodes that may be missing functionality, so for example they may search the DHT for peers or content providers on behalf of IPFS implementations that do not have a DHT.
The delegate node is started and the client of the delegate calls API methods using the IPFS HTTP API client.
## How do I do it?
If you need to support Delegated Content and/or Peer Routing, you can enable it by specifying the multiaddrs of your delegate nodes in the config via `options.config.Addresses.Delegates`. If you need to run a delegate router we encourage you to run your own, with go-ipfs. You can see instructions for doing so in the [delegated routing example](https://github.com/libp2p/js-libp2p/tree/master/examples/delegated-routing).
If you are not able to run your own delegate router nodes, we currently have two nodes that support delegated routing. **Important**: As many people may be leveraging these nodes, performance may be affected, which is why we recommend running your own nodes in production.
Available delegate multiaddrs are:
- `/dns4/node0.delegate.ipfs.io/tcp/443/https`
- `/dns4/node1.delegate.ipfs.io/tcp/443/https`
- `/dns4/node2.delegate.ipfs.io/tcp/443/https`
- `/dns4/node3.delegate.ipfs.io/tcp/443/https`
**Note**: If more than 1 delegate multiaddr is specified, the actual delegate will be randomly selected on startup.
**Note**: If you wish to use delegated routing and are creating your node _programmatically_ in Node.js or the browser you must `npm install libp2p-delegated-content-routing` and/or `npm install libp2p-delegated-peer-routing` and provide configured instances of them in [`options.libp2p`](./MODULE.md#optionslibp2p). See the module repos for further instructions:
- https://github.com/libp2p/js-libp2p-delegated-content-routing
- https://github.com/libp2p/js-libp2p-delegated-peer-routing
================================================
FILE: docs/DEVELOPMENT.md
================================================
# Development
> Getting started with development on IPFS
- [Install npm@7](#install-npm7)
- [Clone and install dependencies](#clone-and-install-dependencies)
- [Run tests](#run-tests)
- [Lint](#lint)
- [Build types and minified browser bundles](#build-types-and-minified-browser-bundles)
- [Publishing new versions](#publishing-new-versions)
- [Using prerelease versions](#using-prerelease-versions)
- [Testing strategy](#testing-strategy)
- [CLI](#cli)
- [HTTP API](#http-api)
- [Core](#core)
- [Non-Core](#non-core)
## Install npm@7
This project uses a [workspace](https://docs.npmjs.com/cli/v7/using-npm/workspaces) structure so requires npm@7 or above. If you are running node 15 or later you already have it, if not run:
```sh
$ npm install -g npm@latest
```
## Clone and install dependencies
```sh
> git clone https://github.com/ipfs/js-ipfs.git
> cd js-ipfs
> npm install
```
This will install the dependencies of the various packages, deduping and hoisting dependencies into the root folder.
If later you add new dependencies to submodules or just wish to remove all the `node_modules`/`dist` folders and start again, run `npm run reset && npm install` from the root.
See the scripts section of the root [`package.json`](../package.json) for more commands.
## Run tests
```sh
# run all the unit tests
> npm test
# run individual tests (findprovs)
> npm run test -- --grep findprovs
# run just IPFS tests in Node.js
> npm run test -- -- -- -t node
# run just IPFS tests in a headless browser
> npm run test -- -- -- -t browser
# run the interface tests against ipfs-core
> npm run test:interface:core
# run the interface tests over HTTP against js-ipfs
> npm run test:interface:http-js
# run the interface tests over HTTP against go-ipfs from a browser
> npm run test:interface:http-go -- -- -- -t browser
# run the interop tests against js-ipfs and go-ipfs on the Electron main process
> npm run test:interop -- -- -- -t electron-main
```
More granular test suites can be run from each submodule.
Please see the `package.json` in each submodule for available commands.
## Lint
Please run the linter before submitting a PR, the build will not pass if it fails:
```sh
> npm run lint
```
## Build types and minified browser bundles
```sh
> npm run build
```
## Publishing new versions
1. Ensure you have a `GH_TOKEN` env var containing a GitHub [Personal Access Token](https://github.com/settings/tokens) with `public_repo` permissions
2. You'll also need a valid [Docker Hub](https://hub.docker.com) login with sufficient permissions to publish new Docker images to the [ipfs/js-ipfs](https://hub.docker.com/repository/docker/ipfs/js-ipfs) repository
3. From the root of this repo run `npm run release` and follow the on screen prompts. It will use [conventional commits](https://www.conventionalcommits.org) to work out the new package version
## Using prerelease versions
Any changed packages from each successful build of master are published to npm as canary builds under the npm tag `next`.
## Testing strategy
This project has a number of components that have their own tests, then some components that share interface tests.
When adding new features you may need to add tests to one or more of the test suites described below.
### CLI
Tests live in [/packages/ipfs/test/cli](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/test/cli).
All interactions with IPFS core are stubbed so we just ensure that the correct arguments are passed in
### HTTP API
Tests live in [/packages/ipfs/test/http-api](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/test/http-api) and are similar to the CLI tests in that we stub out core interactions and inject requests with [shot](https://www.npmjs.com/package/@hapi/shot).
### Core
Anything non-implementation specific should be considered part of the 'Core API'. For example node setup code is not Core, but anything that does useful work, e.g. network/repo/etc interactions would be Core.
All Core APIs should be documented in [/docs/core-api](https://github.com/ipfs/js-ipfs/tree/master/docs/core-api).
All Core APIs should have comprehensive tests in [/packages/interface-ipfs-core](https://github.com/ipfs/js-ipfs/tree/master/packages/interface-ipfs-core).
`interface-ipfs-core` should ensure API compatibility across implementations. Tests are run:
1. Against [/packages/ipfs/src/core](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/core) directly
1. Against [/packages/ipfs/src/http](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/src/http) over HTTP via `ipfs-http-client`
1. Against `go-ipfs` over HTTP via `ipfs-http-client`
### Non-Core
Any non-core API functionality should have tests in the `tests` directory of the module in question, for example: [/packages/ipfs-http-api/tests](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs-http-client/test) and [/packages/ipfs/tests](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs/test) for `ipfs-http-client` and `ipfs` respectively.
================================================
FILE: docs/DOCKER.md
================================================
# Running js-ipfs with Docker
We have automatic Docker builds setup with Docker Hub: https://hub.docker.com/r/ipfs/js-ipfs/
All branches in the Github repository maps to a tag in Docker Hub, except `master` Git branch which is mapped to `latest` Docker tag.
You can run js-ipfs like this:
```
$ docker run -it -p 4002:4002 -p 4003:4003 -p 5002:5002 -p 9090:9090 ipfs/js-ipfs:latest
initializing ipfs node at /root/.jsipfs
generating 2048-bit RSA keypair...done
peer identity: Qmbd5jx8YF1QLhvwfLbCTWXGyZLyEJHrPbtbpRESvYs4FS
to get started, enter:
jsipfs files cat /ipfs/QmfGBRT6BbWJd7yUc2uYdaUZJBbnEFvTqehPFoSMQ6wgdr/readme
Initializing daemon...
Using wrtc for webrtc support
Swarm listening on /ip4/127.0.0.1/tcp/4003/ws/ipfs/Qmbd5jx8YF1QLhvwfLbCTWXGyZLyEJHrPbtbpRESvYs4FS
Swarm listening on /ip4/172.17.0.2/tcp/4003/ws/ipfs/Qmbd5jx8YF1QLhvwfLbCTWXGyZLyEJHrPbtbpRESvYs4FS
Swarm listening on /ip4/127.0.0.1/tcp/4002/ipfs/Qmbd5jx8YF1QLhvwfLbCTWXGyZLyEJHrPbtbpRESvYs4FS
Swarm listening on /ip4/172.17.0.2/tcp/4002/ipfs/Qmbd5jx8YF1QLhvwfLbCTWXGyZLyEJHrPbtbpRESvYs4FS
API is listening on: /ip4/0.0.0.0/tcp/5002
Gateway (readonly) is listening on: /ip4/0.0.0.0/tcp/9090
Daemon is ready
$ curl --silent localhost:5002/api/v0/id | jq .ID
"Qmbd5jx8YF1QLhvwfLbCTWXGyZLyEJHrPbtbpRESvYs4FS"
```
================================================
FILE: docs/EARLY_TESTERS.md
================================================
# Early Testers Programme
- [What is it?](#what-is-it)
- [What are the expectations?](#what-are-the-expectations)
- [Who has signed up?](#who-has-signed-up)
- [How to sign up?](#how-to-sign-up)
## What is it?
The early testers programme allows groups using js-ipfs in production to self-volunteer to help test js-ipfs release candidates to ensure that no regressions that might affect production systems make it into the final release. While we invite the _entire_ community to help test releases, members of the early testers program are expected to participate directly and actively in every release.
## What are the expectations?
Members of the early tester program are expected to work closely with us to:
* Provide high quality, actionable feedback.
* Work directly with us to debug regressions in the release.
* Help ensure a rock-solid, timely release.
We will ask early testers to participate at two points in the process:
* When js-ipfs enters the second release stage, early testers will be asked to test js-ipfs on non-production infrastructure. This may involve things like:
- Running integration tests against the release candidate.
- Running simulations/benchmarks on the release candidate.
- Manually testing the release candidate to check for regressions.
* When js-ipfs enters the third release stage (soft release), early testers will be asked to partially deploy the release candidate to production infrastructure. Release candidates at this stage are expected to be identical to the final release. However, this stage allows the js-ipfs team to fix any last-minute regressions without cutting an entirely new release.
## Who has signed up?
- [npm-on-ipfs](https://github.com/ipfs-shipyard/npm-on-ipfs) - install your dependencies via the distributed web!
- [orbit-db](https://github.com/orbitdb/orbit-db) - Peer-to-Peer Databases for the Decentralized Web
- [ipfs-log](https://github.com/orbitdb/ipfs-log) - Append-only log CRDT on IPFS
- [Sidetree DID Protocol](https://github.com/decentralized-identity/sidetree) - Decentralized Identifier Layer-2 network protocol
- [Constellation](https://julienmalard.github.io/constellation/) - Distributed scientific databases for citizen science and more
## How to sign up?
Simply submit a PR to this document by adding your project name and contact.
================================================
FILE: docs/FAQ.md
================================================
# FAQ
## Table of Contents
- [Why isn't there DHT support in js-IPFS?](#why-isnt-there-dht-support-in-js-ipfs)
- [Node.js](#nodejs)
- [Browser](#browser)
- [How to enable WebRTC support for js-ipfs in the Browser](#how-to-enable-webrtc-support-for-js-ipfs-in-the-browser)
- [Is there WebRTC support for js-ipfs with Node.js?](#is-there-webrtc-support-for-js-ipfs-with-nodejs)
- [How can I configure an IPFS node to use a custom `signaling endpoint` for my WebRTC transport?](#how-can-i-configure-an-ipfs-node-to-use-a-custom-signaling-endpoint-for-my-webrtc-transport)
- [I see some slowness when hopping between tabs Chrome with IPFS nodes, is there a reason why?](#i-see-some-slowness-when-hopping-between-tabs-chrome-with-ipfs-nodes-is-there-a-reason-why)
- [Can I use IPFS in my Electron App?](#can-i-use-ipfs-in-my-electron-app)
- [What are all these `refs?Qmfoo` HTTP errors I keep seeing in the console?](#what-are-all-these-refsqmfoo-http-errors-i-keep-seeing-in-the-console)
- [Have more questions?](#have-more-questions)
## Why isn't there DHT support in js-IPFS?
There is DHT support for js-IPFS in the form of [libp2p/js-libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht) but it is not finished yet, and may not be the right solution to the problem.
### Node.js
To enable DHT support, before starting your daemon run:
```console
$ jsipfs config Routing.Type dht
```
The possible values for `Routing.Type` are:
- `'none'` the default, this means the DHT is turned off any you must manually dial other nodes
- `'dht'` start the node in DHT client mode, if it is discovered to be publicly dialable it will automatically switch to server mode
- `'dhtclient'` A DHT client is able to make DHT queries but will not respond to any
- `'dhtserver'` A DHT server can make and respond to DHT queries. Please only choose this option if your node is dialable from the open Internet.
At the time of writing, only DHT client mode is supported and will be selected if `Routing.Type` is not `'none'`.
### Browser
In the browser there are many constraints that mean the environment does not typically make for good DHT participants - the number of connections required is high, people do not tend to stay on a page for long enough to make or answer DHT queries, and even if they did, most nodes on the network talk TCP - the browser can neither open TCP ports on remote hosts nor accept TCP connections.
A better approach may be to set up [Delegate Routing](./DELEGATE_ROUTERS.md) to use remote go-IPFS to make queries on the browsers' behalf as these do not have the same constraints.
Of course, there's no reason why js on the server should not be a fully fledged DHT participant, please help out on the [libp2p/js-libp2p-kad-dht](https://github.com/libp2p/js-libp2p-kad-dht) repo to make this a reality!
## How to enable WebRTC support for js-ipfs in the Browser
To add a WebRTC transport to your js-ipfs node, you must add a WebRTC multiaddr. To do that, simple override the config.Addresses.Swarm array which contains all the multiaddrs which the IPFS node will use. See below:
```JavaScript
const node = await IPFS.create({
config: {
Addresses: {
Swarm: [
'/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star',
'/dns4/wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star'
]
}
}
})
// your instance with WebRTC is ready
```
**Important:** This transport usage is kind of unstable and several users have experienced crashes. Track development of a solution at https://github.com/ipfs/js-ipfs/issues/1088.
## Is there WebRTC support for js-ipfs with Node.js?
Yes, however, bear in mind that there isn't a 100% stable solution to use WebRTC in Node.js, use it at your own risk. The most tested options are:
- [wrtc](https://npmjs.org/wrtc) - Follow the install instructions.
- [electron-webrtc](https://npmjs.org/electron-webrtc)
To add WebRTC support in a IPFS node instance, do:
```JavaScript
import wrtc from 'wrtc' // or 'electron-webrtc'
import WebRTCStar from '@libp2p/webrtc-star'
const node = await IPFS.create({
repo: 'your-repo-path',
config: {
Addresses: {
Swarm: [
"/ip4/0.0.0.0/tcp/4002",
"/ip4/127.0.0.1/tcp/4003/ws",
"/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star",
"/dns4/wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star"
]
}
},
libp2p: {
modules: {
transport: [WebRTCStar]
},
config: {
peerDiscovery: {
webRTCStar: { // <- note the lower-case w - see https://github.com/libp2p/js-libp2p/issues/576
enabled: true
}
},
transport: {
WebRTCStar: { // <- note the upper-case w- see https://github.com/libp2p/js-libp2p/issues/576
wrtc
}
}
}
}
})
// your instance with WebRTC is ready
```
To add WebRTC support to the IPFS daemon, you only need to install one of the WebRTC modules globally:
```bash
npm install wrtc --global
# or
npm install electron-webrtc --global
```
Then, update your IPFS Daemon config to include the multiaddr for this new transport on the `Addresses.Swarm` array. Add: `"/dns4/wrtc-star.discovery.libp2p.io/wss/p2p-webrtc-star"`
## How can I configure an IPFS node to use a custom `signaling endpoint` for my WebRTC transport?
You'll need to execute a compatible `signaling server` ([libp2p-webrtc-star](https://github.com/libp2p/js-libp2p-webrtc-star) works) and include the correct configuration param for your IPFS node:
- provide the [`multiaddr`](https://github.com/multiformats/multiaddr) for the `signaling server`
```JavaScript
const node = await IPFS.create({
repo: 'your-repo-path',
config: {
Addresses: {
Swarm: [
'/ip4/127.0.0.1/tcp/9090/ws/p2p-webrtc-star'
]
}
}
})
```
The code above assumes you are running a local `signaling server` on port `9090`. Provide the correct values accordingly.
## I see some slowness when hopping between tabs Chrome with IPFS nodes, is there a reason why?
Yes, unfortunately, due to [Chrome aggressive resource throttling policy](https://github.com/ipfs/js-ipfs/issues/611), it cuts freezes the execution of any background tab, turning an IPFS node that was running on that webpage into a vegetable state.
A way to mitigate this in Chrome, is to run your IPFS node inside a Service Worker, so that the IPFS instance runs in a background process. You can learn how to install an IPFS node as a service worker in here the repo [ipfs-service-worker](https://github.com/ipfs/ipfs-service-worker)
## Can I use IPFS in my Electron App?
Yes you can and in many ways. Read https://github.com/ipfs/notes/issues/256 for the multiple options.
We now support Electron v5.0.0 without the need to rebuilt native modules.
Still if you run into problems with native modules follow these instructions [here](https://electronjs.org/docs/tutorial/using-native-node-modules).
## What are all these `refs?Qmfoo` HTTP errors I keep seeing in the console?
In order for content added to your node to be accessible to other nodes on the network, they need to be able to [dial](https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/SWARM.md#ipfsswarmconnectaddr-options) your node. This means there needs to be some way of connecting to you from the open Internet.
From node.js and Electron this might be done by opening a TCP port on your router and forwarding traffic to your node, while also configuring an [Announce](https://github.com/ipfs/js-ipfs/blob/master/docs/CONFIG.md#announce) address that is a combination of the forwarded port and your public IP address.
Browsers [can't open TCP sockets](https://github.com/ipfs/js-ipfs/blob/master/docs/BROWSERS.md#limitations-of-the-browser-context) so the only way right now is for your node to be connected to a WebRTC-Star signalling server - nodes interested in your content would connect to the same WebRTC-Star server and use that to negotiate a peer-to-peer connection.
This has several drawbacks - WebRTC is expensive so having lots of peers does not scale well, the maximum packet size is small so it's comparatively inefficient, browsers will frequently cull connections if you switch away from the tab and at the time of writing go-IPFS [has no WebRTC-Star transport](https://libp2p.io/implementations/#transports) so great swathes of the network will not be able to dial your node.
To make your content available, several 'preload' nodes are running. These nodes expose their [refs endpoint](https://docs.ipfs.io/reference/http/api/#api-v0-refs) over HTTP and all js-IPFS nodes connect to them as peers on startup.
When you add content to your node, a request is sent to a preload node with the CID of the content you've just added. This causes the preload node to use [Bitswap](https://docs.ipfs.io/concepts/bitswap/) to pull the content from your node, caching it for an hour or so which then means other nodes can then access the content without having to dial your otherwise undialable node.
These nodes sometimes go down, which is why you see errors in the console. They are non-fatal and can be ignored.
If you run your own node you can [disable preloading](https://github.com/ipfs/js-ipfs/blob/master/docs/MODULE.md#optionspreload) which will make the errors go away, at the cost of your content becoming less available or not available at all.
## Have more questions?
Ask for help in our forum at https://discuss.ipfs.io or in IRC (#ipfs on Freenode).
================================================
FILE: docs/IPLD.md
================================================
# IPLD Codecs
## Table of Contents
- [Overview](#overview)
- [Bundled BlockCodecs](#bundled-blockcodecs)
- [Bundled Multihashes](#bundled-multihashes)
- [Bundled Multibases](#bundled-multibases)
- [Adding additional BlockCodecs, Multihashes and Multibases](#adding-additional-blockcodecs-multihashes-and-multibases)
- [Next steps](#next-steps)
## Overview
The IPFS repo contains a blockstore that holds the data that makes up the files on the IPFS network. These blocks can be thought of as a [CID][] and associated byte array.
The [CID][] contains a `code` property that lets us know how to interpret the byte array associated with it.
In order to perform that interpretation, a [BlockCodec][] must be loaded that corresponds to the `code` property of the [CID][].
Similarly implementations of [Multihash][]es or [Multibase][]s must be available to be used.
## Bundled BlockCodecs
js-IPFS ships with four bundled codecs, the ones that are required to create and interpret [UnixFS][] structures.
These are:
1. [@ipld/dag-pb](https://github.com/ipld/js-dag-pb) - used for file and directory structures
2. [raw](https://github.com/multiformats/js-multiformats/blob/master/src/codecs/raw.js) - used for file data where imported with `--raw-leaves=true`
3. [@ipld/dag-cbor](https://github.com/ipld/js-dag-cbor) - used for storage of JavaScript Objects with [CID] links to other blocks
4. [json](https://github.com/multiformats/js-multiformats/blob/master/src/codecs/json.js) - used for storage of plain JavaScript Objects
## Bundled Multihashes
js-IPFS ships with all multihashes [exported by js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/src/hashes), including `sha2-256` and others.
Additional hashers can be configured using the `hashers` config property.
## Bundled Multibases
js-IPFS ships with all multibases [exported by js-multiformats](https://github.com/multiformats/js-multiformats/tree/master/src/bases), including `base58btc`, `base32` and others.
Additional bases can be configured using the `bases` config property.
## Adding additional BlockCodecs, Multihashes and Multibases
If your application requires support for extra codecs, you can configure them as follows:
1. Configure the [IPLD layer](https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs/docs/MODULE.md#optionsipld) of your IPFS daemon to support the codec. This step is necessary so the node knows how to prepare data received over HTTP to be passed to IPLD for serialization:
```javascript
import { create } from 'ipfs'
import customBlockCodec from 'custom-blockcodec'
import customMultibase from 'custom-multibase'
import customMultihasher from 'custom-multihasher'
const node = await create({
ipld: {
// either specify BlockCodecs as part of the `codecs` list
codecs: [
customBlockCodec
],
// and/or supply a function to load them dynamically
loadCodec: async (codecNameOrCode) => {
return import(codecNameOrCode)
},
// either specify Multibase codecs as part of the `bases` list
bases: [
customMultibase
],
// and/or supply a function to load them dynamically
loadBase: async (baseNameOrCode) => {
return import(baseNameOrCode)
},
// either specify Multihash hashers as part of the `hashers` list
hashers: [
customMultihasher
],
// and/or supply a function to load them dynamically
loadHasher: async (hashNameOrCode) => {
return import(hashNameOrCode)
}
}
})
```
2. Configure your IPFS HTTP API Client to support the codec. This is necessary so that the client can send the data to the IPFS node over HTTP:
```javascript
import { create } from 'ipfs-http-client'
import customBlockCodec from 'custom-blockcodec'
import customMultibase from 'custom-multibase'
import customMultihasher from 'custom-multihasher'
const client = create({
url: 'http://127.0.0.1:5002',
ipld: {
// either specify BlockCodecs as part of the `codecs` list
codecs: [
customBlockCodec
],
// and/or supply a function to load them dynamically
loadCodec: async (codecNameOrCode) => {
return import(codecNameOrCode)
},
// either specify Multibase codecs as part of the `bases` list
bases: [
customMultibase
],
// and/or supply a function to load them dynamically
loadBase: async (baseNameOrCode) => {
return import(baseNameOrCode)
},
// either specify Multihash hashers as part of the `hashers` list
hashers: [
customMultihasher
],
// and/or supply a function to load them dynamically
loadHasher: async (hashNameOrCode) => {
return import(hashNameOrCode)
}
}
})
```
## Next steps
* See [examples/custom-ipld-formats](https://github.com/ipfs-examples/js-ipfs-examples/tree/master/examples/custom-ipld-formats) for runnable code that demonstrates the above with in-process IPFS nodes, IPFS run as a daemon and also the http client
* Also [examples/traverse-ipld-graphs](https://github.com/ipfs-examples/js-ipfs-examples/tree/master/examples/traverse-ipld-graphs) which uses the [ipld-format-to-blockcodec](https://www.npmjs.com/package/ipld-format-to-blockcodec) module to use older [IPLD format][]s that have not been ported over to the new [BlockCodec][] interface, as well as additional [Multihash Hashers](https://www.npmjs.com/package/multiformats#multihash-hashers).
[cid]: https://docs.ipfs.io/concepts/content-addressing/
[blockcodec]: https://www.npmjs.com/package/multiformats#multicodec-encoders--decoders--codecs
[unixfs]: https://github.com/ipfs/specs/blob/master/UNIXFS.md
[ipld format]: https://github.com/ipld/interface-ipld-format
[multihash]: https://github.com/multiformats/multihash
[multibase]: https://github.com/multiformats/multibase
================================================
FILE: docs/MIGRATION-TO-ASYNC-AWAIT.md
================================================
# Migrating to the new JS IPFS Core API in 0.48.0
A migration guide for refactoring your application code to use the new JS IPFS core API.
Impact key:
* 🍏 easy - simple refactoring in application code
* 🍋 medium - involved refactoring in application code
* 🍊 hard - complicated refactoring in application code
## Table of Contents
- [Migrating from callbacks](#migrating-from-callbacks)
- [Migrating from `PeerId`](#migrating-from-peerid)
- [Migrating from `PeerInfo`](#migrating-from-peerinfo)
- [Migrating to Async Iterables](#migrating-to-async-iterables)
- [From Node.js Streams](#from-nodejs-streams)
- [Node.js Readable Streams](#nodejs-readable-streams)
- [Piping Node.js Streams](#piping-nodejs-streams)
- [Node.js Transform Streams](#nodejs-transform-streams)
- [From Pull Streams](#from-pull-streams)
- [Source Pull Streams](#source-pull-streams)
- [Pull Stream Pipelines](#pull-stream-pipelines)
- [Transform Pull Streams](#transform-pull-streams)
- [From buffering APIs](#from-buffering-apis)
- [Migrating from `addFromFs`](#migrating-from-addfromfs)
- [Migrating from `addFromURL`](#migrating-from-addfromurl)
- [Migrating from `addFromStream`](#migrating-from-addfromstream)
## Migrating from callbacks
Callbacks are no longer supported in the API. If your application primarily uses callbacks you have two main options for migration:
**Impact 🍊**
Switch to using the promise API with async/await. Instead of program continuation in a callback, continuation occurs after the async call and functions from where the call is made are changed to be async functions.
e.g.
```js
function main () {
ipfs.id((err, res) => {
console.log(res)
})
}
main()
```
Becomes:
```js
async function main () {
const res = await ipfs.id()
console.log(res)
}
main()
```
**Impact 🍏**
Alternatively you could "callbackify" the API. In this case you use a module to convert the promise API to a callback API either permanently or in an interim period.
e.g.
```js
function main () {
ipfs.id((err, res) => {
console.log(res)
})
}
main()
```
Becomes:
```js
const callbackify = require('callbackify')
const ipfsId = callbackify(ipfs.id)
async function main () {
ipfsId((err, res) => {
console.log(res)
})
}
main()
```
## Migrating from `PeerId`
Libp2p `PeerId` instances are no longer returned from the API. If your application is using the crypto capabilities of [`PeerId`](https://github.com/libp2p/js-peer-id) instances then you'll want to convert the peer ID `string` returned by the new API back into libp2p `PeerId` instances.
**Impact 🍏**
Peer ID strings are also CIDs so converting them is simple:
```js
const peerId = PeerId.createFromB58String(peerIdStr)
```
You can get hold of the `PeerId` class using npm or in a script tag:
```js
import { PeerId } from '@libp2p/interface-peer-id'
const peerId = PeerId.createFromB58String(peerIdStr)
```
```html
```
## Migrating from `PeerInfo`
Libp2p `PeerInfo` instances are no longer returned from the API. Instead, plain objects of the form `{ id: string, addrs: Multiaddr[] }` are returned. To convert these back into a `PeerInfo` instance:
**Impact 🍏**
Instantiate a new `PeerInfo` and add addresses to it:
```js
const peerInfo = new PeerInfo(PeerId.createFromB58String(info.id))
info.addrs.forEach(addr => peerInfo.multiaddrs.add(addr))
```
You can get hold of the `PeerInfo` class using npm or in a script tag:
```js
const PeerInfo = require('peer-info')
import { PeerId } from '@libp2p/interface-peer-id'
const peerInfo = new PeerInfo(PeerId.createFromB58String(info.id))
info.addrs.forEach(addr => peerInfo.multiaddrs.add(addr))
```
```html
```
## Migrating to Async Iterables
Async Iterables are a language native way of streaming data. The IPFS core API has previously supported two different stream implementations - Pull Streams and Node.js Streams. Similarly to those two different implementations, streaming iterables come in different forms for different purposes:
1. **source** - something that can be consumed. Analogous to a "source" pull stream or a "readable" Node.js stream
2. **sink** - something that consumes (or drains) a source. Analogous to a "sink" pull stream or a "writable" Node.js stream
3. **transform** - both a sink and a source where the values it consumes and the values that can be consumed from it are connected in some way. Analogous to a transform in both Pull and Node.js streams
4. **duplex** - similar to a transform but the values it consumes are not necessarily connected to the values that can be consumed from it
More information and examples here: https://gist.github.com/alanshaw/591dc7dd54e4f99338a347ef568d6ee9
List of useful modules for working with async iterables: https://github.com/alanshaw/it-awesome
Note that iterables might gain many helper functions soon: https://github.com/tc39/proposal-iterator-helpers
### From Node.js Streams
#### Node.js Readable Streams
Modern Node.js readable streams are async iterable so there's no changes to any APIs that you'd normally pass a stream to. The `*ReadableStream` APIs have been removed. To migrate from `*ReadableStream` methods, there are a couple of options:
**Impact 🍊**
Use a [for/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) loop to consume an async iterable.
e.g.
```js
const readable = ipfs.catReadableStream('QmHash')
const decoder = new TextDecoder()
readable.on('data', chunk => {
console.log(decoder.decode(chunk))
})
readable.on('end', () => {
console.log('done')
})
```
Becomes:
```js
const source = ipfs.cat('QmHash')
const decoder = new TextDecoder()
for await (const chunk of source) {
console.log(decoder.decode(chunk))
}
console.log('done')
```
**Impact 🍏**
Convert the async iterable to a readable stream.
e.g.
```js
const readable = ipfs.catReadableStream('QmHash')
const decoder = new TextDecoder()
readable.on('data', chunk => {
console.log(decoder.decode(chunk))
})
readable.on('end', () => {
console.log('done')
})
```
Becomes:
```js
import toStream from 'it-to-stream'
const readable = toStream.readable(ipfs.cat('QmHash'))
const decoder = new TextDecoder()
readable.on('data', chunk => {
console.log(decoder.decode(chunk))
})
readable.on('end', () => {
console.log('done')
})
```
#### Piping Node.js Streams
Sometimes applications will "pipe" Node.js streams together, using the `.pipe` method or the `pipeline` utility. There are 2 possible migration options:
**Impact 🍊**
Use `it-pipe` and a [for/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) loop to concat data from an async iterable.
e.g.
```js
const { pipeline, Writable } = require('stream')
const decoder = new TextDecoder()
let data = new Uint8Array(0)
const concat = new Writable({
write (chunk, enc, cb) {
data = uint8ArrayConcat([data, chunk])
cb()
}
})
pipeline(
ipfs.catReadableStream('QmHash'),
concat,
err => {
console.log(decoder.decode(chunk))
}
)
```
Becomes:
```js
const pipe = require('it-pipe')
const decoder = new TextDecoder()
let data = new Uint8Array(0)
const concat = async source => {
for await (const chunk of source) {
data = uint8ArrayConcat([data, chunk])
}
}
const data = await pipe(
ipfs.cat('QmHash'),
concat
)
console.log(decoder.decode(data))
```
...which, by the way, could more succinctly be written as:
```js
import toBuffer from 'it-to-buffer'
const decoder = new TextDecoder()
const data = await toBuffer(ipfs.cat('QmHash'))
console.log(decoder.decode(data))
```
**Impact 🍏**
Convert the async iterable to a readable stream.
e.g.
```js
const { pipeline, Writable } = require('stream')
const decoder = new TextDecoder()
let data = new Uint8Array(0)
const concat = new Writable({
write (chunk, enc, cb) {
data = uint8ArrayConcat([data, chunk])
cb()
}
})
pipeline(
ipfs.catReadableStream('QmHash'),
concat,
err => {
console.log(decoder.decode(data))
}
)
```
Becomes:
```js
import toStream from 'it-to-stream'
const { pipeline, Writable } = require('stream')
const decoder = new TextDecoder()
let data = new Uint8Array(0)
const concat = new Writable({
write (chunk, enc, cb) {
data = uint8ArrayConcat([data, chunk])
cb()
}
})
pipeline(
toStream.readable(ipfs.cat('QmHash')),
concat,
err => {
console.log(decoder.decode(data))
}
)
```
#### Node.js Transform Streams
Commonly in Node.js you have a readable stream of a file from the filesystem that you want to add to IPFS. There are 2 possible migration options:
**Impact 🍊**
Use `it-pipe` and a [for/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) loop to collect all items from an async iterable.
e.g.
```js
import fs from 'fs'
const { pipeline } = require('stream')
const items = []
const all = new Writable({
objectMode: true,
write (chunk, enc, cb) {
items.push(chunk)
cb()
}
})
pipeline(
fs.createReadStream('/path/to/file'),
ipfs.addReadableStream(),
all,
err => {
console.log(items)
}
)
```
Becomes:
```js
import fs from 'fs'
const pipe = require('it-pipe')
const items = []
const all = async source => {
for await (const chunk of source) {
items.push(chunk)
}
}
await pipe(
fs.createReadStream('/path/to/file'), // Because Node.js streams are iterable
ipfs.add,
all
)
console.log(items)
```
...which, by the way, could more succinctly be written as:
```js
import fs from 'fs'
const pipe = require('it-pipe')
import all from 'it-all'
const items = await pipe(
fs.createReadStream('/path/to/file'),
ipfs.add,
all
)
console.log(items)
```
**Impact 🍏**
Convert the async iterable to a readable stream.
e.g.
```js
import fs from 'fs'
const { pipeline } = require('stream')
const items = []
const all = new Writable({
objectMode: true,
write (chunk, enc, cb) {
items.push(chunk)
cb()
}
})
pipeline(
fs.createReadStream('/path/to/file'),
ipfs.addReadableStream(),
all,
err => {
console.log(items)
}
)
```
Becomes:
```js
import toStream from 'it-to-stream'
import fs from 'fs'
const { pipeline } = require('stream')
const items = []
const all = new Writable({
objectMode: true,
write (chunk, enc, cb) {
items.push(chunk)
cb()
}
})
pipeline(
fs.createReadStream('/path/to/file'),
toStream.transform(ipfs.add),
all,
err => {
console.log(items)
}
)
```
### From Pull Streams
#### Source Pull Streams
Pull Streams can no longer be passed to IPFS API methods and the `*PullStream` APIs have been removed. To pass a pull stream directly to an IPFS API method, first convert it to an async iterable using [`pull-stream-to-async-iterator`](https://www.npmjs.com/package/pull-stream-to-async-iterator). To migrate from `*PullStream` methods, there are a couple of options:
**Impact 🍊**
Use a [for/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) loop to consume an async iterable.
e.g.
```js
const decoder = new TextDecoder()
pull(
ipfs.catPullStream('QmHash'),
pull.through(chunk => {
console.log(decoder.decode(data))
}),
pull.onEnd(err => {
console.log('done')
})
)
```
Becomes:
```js
const decoder = new TextDecoder()
for await (const chunk of ipfs.cat('QmHash')) {
console.log(decoder.decode(data))
}
console.log('done')
```
**Impact 🍏**
Convert the async iterable to a pull stream.
e.g.
```js
const decoder = new TextDecoder()
pull(
ipfs.catPullStream('QmHash'),
pull.through(chunk => {
console.log(decoder.decode(data))
}),
pull.onEnd(err => {
console.log('done')
})
)
```
Becomes:
```js
const toPull = require('async-iterator-to-pull-stream')
const decoder = new TextDecoder()
pull(
toPull.source(ipfs.cat('QmHash')),
pull.through(chunk => {
console.log(decoder.decode(data))
}),
pull.onEnd(err => {
console.log('done')
})
)
```
#### Pull Stream Pipelines
Frequently, applications will use `pull()` to create a pipeline of pull streams.
**Impact 🍊**
Use `it-pipe` and `it-concat` concat data from an async iterable.
e.g.
```js
const decoder = new TextDecoder()
pull(
ipfs.catPullStream('QmHash'),
pull.collect((err, chunks) => {
console.log(decoder.decode(uint8ArrayConcat(chunks)))
})
)
```
Becomes:
```js
const pipe = require('it-pipe')
import concat from 'it-concat'
const decoder = new TextDecoder()
const data = await pipe(
ipfs.cat('QmHash'),
concat
)
console.log(decoder.decode(data))
```
#### Transform Pull Streams
You might have a pull stream source of a file from the filesystem that you want to add to IPFS. There are 2 possible migration options:
**Impact 🍊**
Use `it-pipe` and `it-all` to collect all items from an async iterable.
e.g.
```js
import fs from 'fs'
const toPull = require('stream-to-pull-stream')
pull(
toPull.source(fs.createReadStream('/path/to/file')),
ipfs.addPullStream(),
pull.collect((err, items) => {
console.log(items)
})
)
```
Becomes:
```js
import fs from 'fs'
const file = await ipfs.add(fs.createReadStream('/path/to/file'))
console.log(file)
```
**Impact 🍏**
Convert the async iterable to a pull stream.
e.g.
```js
import fs from 'fs'
const toPull = require('stream-to-pull-stream')
pull(
toPull.source(fs.createReadStream('/path/to/file')),
ipfs.addPullStream(),
pull.collect((err, items) => {
console.log(items)
})
)
```
Becomes:
```js
import fs from 'fs'
const streamToPull = require('stream-to-pull-stream')
const itToPull = require('async-iterator-to-pull-stream')
pull(
streamToPull.source(fs.createReadStream('/path/to/file')),
itToPull.transform(ipfs.add),
pull.collect((err, items) => {
console.log(items)
})
)
```
### From buffering APIs
The old APIs like `ipfs.add`, `ipfs.cat`, `ipfs.ls` and others were "buffering APIs" i.e. they collect all the results into memory before returning them. The new JS core interface APIs are streaming by default in order to reduce memory usage, reduce time to first byte and to provide better feedback. The following are examples of switching from the old `ipfs.add`, `ipfs.cat` and `ipfs.ls` to the new APIs:
**Impact 🍏**
Adding files.
e.g.
```js
const results = await ipfs.addAll([
{ path: 'root/1.txt', content: 'one' },
{ path: 'root/2.txt', content: 'two' }
])
// Note that ALL files have already been added to IPFS
results.forEach(file => {
console.log(file.path)
})
```
Becomes:
```js
const addSource = ipfs.addAll([
{ path: 'root/1.txt', content: 'one' },
{ path: 'root/2.txt', content: 'two' }
])
for await (const file of addSource) {
console.log(file.path) // Note these are logged out as they are added
}
```
Alternatively you can buffer up the results using the `it-all` utility:
```js
import all from 'it-all'
const results = await all(ipfs.addAll([
{ path: 'root/1.txt', content: 'one' },
{ path: 'root/2.txt', content: 'two' }
]))
results.forEach(file => {
console.log(file.path)
})
```
Often you just want the last item (the root directory entry) when adding multiple files to IPFS:
```js
const results = await ipfs.addAll([
{ path: 'root/1.txt', content: 'one' },
{ path: 'root/2.txt', content: 'two' }
])
const lastResult = results[results.length - 1]
console.log(lastResult)
```
Becomes:
```js
const addSource = ipfs.addAll([
{ path: 'root/1.txt', content: 'one' },
{ path: 'root/2.txt', content: 'two' }
])
let lastResult
for await (const file of addSource) {
lastResult = file
}
console.log(lastResult)
```
Alternatively you can use the `it-last` utility:
```js
const lastResult = await last(ipfs.addAll([
{ path: 'root/1.txt', content: 'one' },
{ path: 'root/2.txt', content: 'two' }
]))
console.log(lastResult)
```
**Impact 🍏**
Reading files.
e.g.
```js
import fs from 'fs'
const data = await ipfs.cat('/ipfs/QmHash')
// Note that here we have read the entire file
// i.e. `data` holds ALL the contents of the file in memory
await fs.writeFile('/tmp/file.iso', data)
console.log('done')
```
Becomes:
```js
const pipe = require('it-pipe')
import toIterable from 'stream-to-it'
import fs from 'fs'
// Note that as chunks arrive they are written to the file and memory can be freed and re-used
await pipe(
ipfs.cat('/ipfs/QmHash'),
toIterable.sink(fs.createWriteStream('/tmp/file.iso'))
)
console.log('done')
```
Alternatively you can buffer up the chunks using the `it-concat` utility (not recommended!):
```js
import fs from 'fs'
import concat from 'it-concat'
const data = await concat(ipfs.cat('/ipfs/QmHash'))
await fs.writeFile('/tmp/file.iso', data.slice())
console.log('done')
```
**Impact 🍏**
Listing directory contents.
e.g.
```js
const files = await ipfs.ls('/ipfs/QmHash')
// Note that ALL files in the directory have been read into memory
files.forEach(file => {
console.log(file.name)
})
```
Becomes:
```js
const filesSource = ipfs.ls('/ipfs/QmHash')
for await (const file of filesSource) {
console.log(file.name) // Note these are logged out as they are retrieved from the network/disk
}
```
Alternatively you can buffer up the directory listing using the `it-all` utility:
```js
import all from 'it-all'
const results = await all(ipfs.ls('/ipfs/QmHash'))
results.forEach(file => {
console.log(file.name)
})
```
## Migrating from `addFromFs`
The `addFromFs` API method has been removed and replaced with a helper function `globSource` that is exported from `js-ipfs`/`js-ipfs-http-client`. See the [API docs for `globSource` for more info](https://github.com/ipfs/js-ipfs-http-client/blob/f30031163b9ac4ce2cff34ad4854f24b23cbff0b/README.md#glob-source).
**Impact 🍏**
e.g.
```js
const IpfsHttpClient = require('ipfs-http-client')
const ipfs = IpfsHttpClient()
const files = await ipfs.addFromFs('./docs', { recursive: true })
files.forEach(file => {
console.log(file)
})
```
Becomes:
```js
const IpfsHttpClient = require('ipfs-http-client')
const { globSource } = IpfsHttpClient
const ipfs = IpfsHttpClient()
for await (const file of ipfs.addAll(globSource('./docs', { recursive: true }))) {
console.log(file)
}
```
## Migrating from `addFromURL`
The `addFromURL` API method has been removed and replaced with a helper function `urlSource` that is exported from `js-ipfs`/`js-ipfs-http-client`. See the [API docs for `urlSource` for more info](https://github.com/ipfs/js-ipfs-http-client/blob/f30031163b9ac4ce2cff34ad4854f24b23cbff0b/README.md#url-source).
**Impact 🍏**
e.g.
```js
const IpfsHttpClient = require('ipfs-http-client')
const ipfs = IpfsHttpClient()
const files = await ipfs.addFromURL('https://ipfs.io/images/ipfs-logo.svg')
files.forEach(file => {
console.log(file)
})
```
Becomes:
```js
const IpfsHttpClient = require('ipfs-http-client')
const { urlSource } = IpfsHttpClient
const ipfs = IpfsHttpClient()
const file = await ipfs.add(urlSource('https://ipfs.io/images/ipfs-logo.svg'))
console.log(file)
```
## Migrating from `addFromStream`
The `addFromStream` API method has been removed. This was an alias for `add`.
**Impact 🍏**
e.g.
```js
const IpfsHttpClient = require('ipfs-http-client')
const ipfs = IpfsHttpClient()
const files = await ipfs.addFromStream(fs.createReadStream('/path/to/file.txt'))
files.forEach(file => {
console.log(file)
})
```
Becomes:
```js
import fs from 'fs'
const ipfs = IpfsHttpClient()
const file = await ipfs.add(fs.createReadStream('/path/to/file.txt'))
console.log(file)
```
================================================
FILE: docs/MODULE.md
================================================
# IPFS Module
Use the IPFS module as a dependency of your project to spawn in process instances of IPFS in node.js, the browser, electron, etc.
## Table of contents
- [Getting started](#getting-started)
- [IPFS.create([options])](#ipfscreateoptions)
- [`options.repo`](#optionsrepo)
- [`options.repoAutoMigrate`](#optionsrepoautomigrate)
- [`options.init`](#optionsinit)
- [`options.start`](#optionsstart)
- [`options.pass`](#optionspass)
- [`options.silent`](#optionssilent)
- [`options.relay`](#optionsrelay)
- [`options.offline`](#optionsoffline)
- [`options.preload`](#optionspreload)
- [`options.EXPERIMENTAL`](#optionsexperimental)
- [`options.config`](#optionsconfig)
- [`options.ipld`](#optionsipld)
- [`options.libp2p`](#optionslibp2p)
- [Instance methods](#instance-methods)
- [`node.start()`](#nodestart)
- [Static types and utils](#static-types-and-utils)
- [Glob source](#glob-source)
- [`globSource(path, pattern, [options])`](#globsourcepath-pattern-options)
- [Example](#example)
- [URL source](#url-source)
- [`urlSource(url)`](#urlsourceurl)
- [Example](#example-1)
- [Path](#path)
- [`path()`](#path-1)
- [Example](#example-2)
## Getting started
Create a running node with:
```javascript
// Create the IPFS node instance
const node = await IPFS.create()
// Your node is now ready to use \o/
await node.stop()
// node is now 'offline'
```
The node returned from `IPFS.create()` supports the [IPFS Core API](https://github.com/ipfs/js-ipfs/tree/master/docs/core-api), along with some additional methods documented below.
## IPFS.create([options])
```js
const node = await IPFS.create([options])
```
Creates and returns a ready to use instance of an IPFS node.
Use the `options` argument to specify advanced configuration. It is an object with any of these properties:
### `options.repo`
| Type | Default |
|------|---------|
| string or [`ipfs.Repo`](https://github.com/ipfs/js-ipfs-repo) instance | `'~/.jsipfs'` in Node.js, `'ipfs'` in browsers |
The file path at which to store the IPFS node’s data. Alternatively, you can set up a customized storage system by providing an [`ipfs.Repo`](https://github.com/ipfs/js-ipfs-repo) instance.
Example:
```js
// Store data outside your user directory
const node = await IPFS.create({ repo: '/var/ipfs/data' })
```
### `options.repoAutoMigrate`
| Type | Default |
| --------- | ------- |
| `boolean` | `true` |
`js-ipfs` comes bundled with a tool that automatically migrates your IPFS repository when a new version is available.
**For apps that build on top of `js-ipfs` and run in the browser environment, be aware that disabling automatic
migrations leaves the user with no way to run the migrations because there is no CLI in the browser. In such
a case, you should provide a way to trigger migrations manually.**
### `options.init`
| Type | Default |
| ----------------- | ------- |
| boolean or object | `true` |
Perform repo initialization steps when creating the IPFS node.
Note that *initializing* a repo is different from creating an instance of [`ipfs.Repo`](https://github.com/ipfs/js-ipfs-repo). The IPFS constructor sets many special properties when initializing a repo, so you should usually not try and call `repoInstance.init()` yourself.
Instead of a boolean, you may provide an object with custom initialization options. All properties are optional:
- `emptyRepo` (boolean) Whether to remove built-in assets, like the instructional tour and empty mutable file system, from the repo. (Default: `false`)
- `algorithm` (string) The type of key to use. Supports `rsa`, `ed25519`, `secp256k1`. (Default: `rsa`)
- `bits` (number) Number of bits to use in the generated key pair (rsa only). (Default: `2048`)
- `privateKey` (string/PeerId) A pre-generated private key to use. Can be either a base64 string or a [PeerId](https://github.com/libp2p/js-peer-id) instance. **NOTE: This overrides `bits`.**
```js
// Generating a Peer ID:
import { PeerId } from '@libp2p/interface-peer-id'
// Generates a new Peer ID, complete with public/private keypair
// See https://github.com/libp2p/js-peer-id
const peerId = await PeerId.create({ bits: 2048 })
```
- `pass` (string) A passphrase to encrypt keys. You should generally use the [top-level `pass` option](#optionspass) instead of the `init.pass` option (this one will take its value from the top-level option if not set).
- `profiles` (Array) Apply profile settings to config.
- `allowNew` (boolean, default: `true`) Set to `false` to disallow initialization if the repo does not already exist.
### `options.start`
| Type | Default |
| --------- | ------- |
| `boolean` | `true` |
If `false`, do not automatically start the IPFS node. Instead, you’ll need to manually call [`node.start()`](#nodestart) yourself.
### `options.pass`
| Type | Default |
| ------ | ------- |
| string | `null` |
A passphrase to encrypt/decrypt your keys.
### `options.silent`
| Type | Default |
| ------- | ------- |
| Boolean | `false` |
Prevents all logging output from the IPFS node.
### `options.relay`
| Type | Default |
|------|---------|
| object | `{ enabled: true, hop: { enabled: false, active: false } }` |
Configure circuit relay (see the [circuit relay tutorial](https://github.com/ipfs-examples/js-ipfs-examples/tree/master/examples/circuit-relaying) to learn more).
- `enabled` (boolean): Enable circuit relay dialer and listener. (Default: `true`)
- `hop` (object)
- `enabled` (boolean): Make this node a relay (other nodes can connect *through* it). (Default: `false`)
- `active` (boolean): Make this an *active* relay node. Active relay nodes will attempt to dial a destination peer even if that peer is not yet connected to the relay. (Default: `false`)
### `options.offline`
| Type | Default |
| ------- | ------- |
| Boolean | `false` |
Run ipfs node offline. The node does not connect to the rest of the network but provides a local API.
### `options.preload`
| Type | Default |
| ------ | ------------------------------------- |
| object | `{ enabled: true, addresses: [...] }` |
Configure remote preload nodes. The remote will preload content added on this node, and also attempt to preload objects requested by this node.
- `enabled` (boolean): Enable content preloading (Default: `true`)
- `addresses` (array): Multiaddr API addresses of nodes that should preload content. **NOTE:** nodes specified here should also be added to your node's bootstrap address list at [`config.Bootstrap`](#optionsconfig).
### `options.EXPERIMENTAL`
| Type | Default |
| ------ | ---------------------------------------- |
| object | `{ ipnsPubsub: false, sharding: false }` |
Enable and configure experimental features.
- `ipnsPubsub` (boolean): Enable pub-sub on IPNS. (Default: `false`)
- `sharding` (boolean): Enable directory sharding. Directories that have many child objects will be represented by multiple DAG nodes instead of just one. It can improve lookup performance when a directory has several thousand files or more. (Default: `false`)
### `options.config`
| Type | Default |
|------|---------|
| object | [`config.js`](https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs-core-config/src/config.js) in Node.js, [`config-browser.js`](https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs-core-config/src/config.browser.js) in browsers |
Modify the default IPFS node config. This object will be *merged* with the default config; it will not replace it. The default config is documented in [the js-ipfs config file docs](./CONFIG.md).
### `options.ipld`
| Type | Default |
|------|---------|
| object | [`ipld.js`](https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs-core-config/src/ipld.js) |
Modify the default IPLD config. This object will be *merged* with the default config; it will not replace it. Check IPLD [docs](https://github.com/ipld/js-ipld#ipld-constructor) for more information on the available options.
> Browser config does **NOT** include by default all the IPLD formats. Only `ipld-dag-pb`, `ipld-dag-cbor` and `ipld-raw` are included.
To add support for other formats we provide two options, one sync and another async.
Examples for the sync option:
ESM Environments
```js
import ipldGit from 'ipld-git'
import ipldBitcoin from 'ipld-bitcoin'
import { convert } from 'ipld-format-to-blockcodec'
const node = await IPFS.create({
ipld: {
codecs: [
convert(ipldGit),
convert(ipldBitcoin)
]
}
})
```
Commonjs Environments
```js
const IPFS = require('ipfs')
const ipldGit = require('ipld-git')
const ipldBitcoin = require('ipld-bitcoin')
const { convert } = require('ipld-format-to-blockcodec')
const node = await IPFS.create({
ipld: {
codecs: [
convert(ipldGit),
convert(ipldBitcoin)
]
}
})
```
Using script tags
```html
```
Examples for the async option:
ESM Environments
```js
const node = await IPFS.create({
ipld: {
async loadCodec (codec) {
if (codec === multicodec.GIT_RAW) {
return convert(await import('ipld-git')) // This is a dynamic import
} else {
throw new Error('unable to load format ' + multicodec.print[codec])
}
}
}
})
```
> For more information about dynamic imports please check [webpack docs](https://webpack.js.org/guides/code-splitting/#dynamic-imports) or search your bundler documention.
Using dynamic imports will tell your bundler to create a separate file (normally called *chunk*) that will **only** be requested by the browser if it's really needed. This strategy will reduce your bundle size and load times without removing any functionality.
With Webpack IPLD formats can even be grouped together using magic comments `import(/* webpackChunkName: "ipld-formats" */ 'ipld-git')` to produce a single file with all of them.
Commonjs Environments
```js
const node = await IPFS.create({
ipld: {
async loadFormat (codec) {
if (codec === multicodec.GIT_RAW) {
return require('ipld-git')
} else {
throw new Error('unable to load format ' + multicodec.print[codec])
}
}
}
})
```
Using Script tags
```js
```
### `options.libp2p`
| Type | Default |
|------|---------|
| object | [`libp2p-nodejs.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs-core-config/src/libp2p-nodejs.js) in Node.js, [`libp2p-browser.js`](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs-core-config/src)/libp2p-browser.js) in browsers |
| function | [`libp2p bundle`](https://github.com/ipfs-examples/js-ipfs-examples/tree/master/examples/custom-libp2p) |
The libp2p option allows you to build your libp2p node by configuration, or via a bundle function. If you are looking to just modify the below options, using the object format is the quickest way to get the default features of libp2p. If you need to create a more customized libp2p node, such as with custom transports or peer/content routers that need some of the ipfs data on startup, a custom bundle is a great way to achieve this.
You can see the bundle in action in the [custom libp2p example](https://github.com/ipfs-examples/js-ipfs-examples/tree/master/examples/custom-libp2p).
Please see [libp2p/docs/CONFIGURATION.md](https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md) for the list of options libp2p supports.
### Instance methods
#### `node.start()`
Start listening for connections with other IPFS nodes on the network. In most cases, you do not need to call this method — `IPFS.create()` will automatically do it for you.
This method is asynchronous and returns a promise.
```js
const node = await IPFS.create({ start: false })
console.log('Node is ready to use but not started!')
try {
await node.start()
console.log('Node started!')
} catch (/** @type {any} */ error) {
console.error('Node failed to start!', error)
}
```
## Static types and utils
Aside from the default export, `ipfs` exports various types and utilities that are included in the bundle:
- [`crypto`](https://www.npmjs.com/package/libp2p-crypto)
- [`isIPFS`](https://www.npmjs.com/package/is-ipfs)
- [`Buffer`](https://www.npmjs.com/package/buffer)
- [`PeerId`](https://docs.libp2p.io/concepts/peer-id/)
- [`PeerInfo`](https://www.npmjs.com/package/peer-info)
- [`multiaddr`](https://www.npmjs.com/package/multiaddr)
- [`multibase`](https://www.npmjs.com/package/multibase)
- [`multihash`](https://www.npmjs.com/package/multihashes)
- [`multihashing`](https://www.npmjs.com/package/multihashing-async)
- [`multicodec`](https://www.npmjs.com/package/multicodec)
- [`CID`](https://docs.ipfs.io/concepts/content-addressing)
These can be accessed like this, for example:
```js
const { CID } = require('ipfs')
// ...or from an es-module:
import { CID } from 'ipfs'
```
##### Glob source
A utility to allow files on the file system to be easily added to IPFS.
###### `globSource(path, pattern, [options])`
- `path`: A path to a single file or directory to glob from
- `pattern`: A pattern to match files under `path`
- `options`: Optional options
- `options.hidden`: Hidden/dot files (files or folders starting with a `.`, for example, `.git/`) are not included by default. To add them, use the option `{ hidden: true }`.
Returns an async iterable that yields `{ path, content }` objects suitable for passing to `ipfs.add`.
###### Example
```js
import { create, globSource } from 'ipfs'
const ipfs = await create()
for await (const file of ipfs.addAll(globSource('./docs', '**/*'))) {
console.log(file)
}
/*
{
path: 'docs/assets/anchor.js',
cid: CID('QmVHxRocoWgUChLEvfEyDuuD6qJ4PhdDL2dTLcpUy3dSC2'),
size: 15347
}
{
path: 'docs/assets/bass-addons.css',
cid: CID('QmPiLWKd6yseMWDTgHegb8T7wVS7zWGYgyvfj7dGNt2viQ'),
size: 232
}
...
*/
```
##### URL source
A utility to allow content from the internet to be easily added to IPFS.
###### `urlSource(url)`
- `url`: A string URL or [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) instance to send HTTP GET request to
Returns an async iterable that yields `{ path, content }` objects suitable for passing to `ipfs.add`.
###### Example
```js
import { create, urlSource } from 'ipfs'
const ipfs = await create()
const file = await ipfs.add(urlSource('https://ipfs.io/images/ipfs-logo.svg'))
console.log(file)
/*
{
path: 'ipfs-logo.svg',
cid: CID('QmTqZhR6f7jzdhLgPArDPnsbZpvvgxzCZycXK7ywkLxSyU'),
size: 3243
}
*/
```
##### Path
A function that returns the path to the js-ipfs CLI.
This is analogous to the `.path()` function exported by the [go-ipfs](https://www.npmjs.com/package/go-ipfs) module.
###### `path()`
Returns the path to the js-ipfs CLI
###### Example
```js
import { path } from 'ipfs'
console.info(path()) // /foo/bar/node_modules/ipfs/src/cli.js
```
================================================
FILE: docs/MONITORING.md
================================================
# Monitoring
The HTTP API exposed with js-ipfs can also be used for exposing metrics about the running js-ipfs node and other Node.js metrics.
To enable it, you need to set the environment variable `IPFS_MONITORING` (any value). E.g.
```console
$ IPFS_MONITORING=true jsipfs daemon
```
Once the environment variable is set and the js-ipfs daemon is running, you can get the metrics (in prometheus format) by making a GET request to the following endpoint:
```
http://localhost:5002/debug/metrics/prometheus
```
================================================
FILE: docs/README.md
================================================
# IPFS Docs
- [API Docs](#api-docs)
- [How tos and other documentation](#how-tos-and-other-documentation)
- [Development documentation](#development-documentation)
## API Docs
`ipfs` can run as part of your program (an in-process node) or as a standalone daemon process that can be communicated with via an HTTP RPC API using the [`ipfs-http-client`](../packages/ipfs-http-client) module.
Whether accessed directly or over HTTP, both methods support the full [Core API](#core-api). In addition other methods are available to construct instances of each module, etc.
* [Core API docs](./core-api/README.md)
* [IPFS API](../packages/ipfs/README.md)
* [IPFS-HTTP-CLIENT API](../packages/ipfs-http-client/README.md)
## How tos and other documentation
* [Architecture overview](./ARCHITECTURE.md)
* [How to run js-IPFS in the browser](./BROWSERS.md)
* [Running js-IPFS on the CLI](./CLI.md)
* [js-IPFS configuration options](./CONFIG.md)
* [How to configure CORS for use with the http client](./CORS.md)
* [Running js-IPFS as a daemon](./DAEMON.md)
* [Configuring Delegate Routers](./DELEGATE_ROUTERS.md)
* [Running js-IPFS under Docker](./DOCKER.md)
* [FAQ](./FAQ.md)
* [How to configure additional IPLD codecs](./IPLD.md)
* [Running js-IPFS in your application](./MODULE.md)
* [How to get metrics out of js-IPFS](./MONITORING.md)
## Development documentation
* [Getting started](./DEVELOPMENT.md)
* [Release issue template](./RELEASE_ISSUE_TEMPLATE.md)
* [Early testers](./EARLY_TESTERS.md)
* [Releases](./RELEASES.md)
================================================
FILE: docs/RELEASES.md
================================================
# Releases
## Table of Contents
- [Release Philosophy](#release-philosophy)
- [Release Flow](#release-flow)
- [Stage 0 - Automated Testing](#stage-0---automated-testing)
- [Stage 1 - Internal Testing](#stage-1---internal-testing)
- [Stage 2 - Community Dev Testing](#stage-2---community-dev-testing)
- [Stage 3 - Community Prod Testing](#stage-3---community-prod-testing)
- [Stage 4 - Release](#stage-4---release)
- [Release Cycle](#release-cycle)
- [Patch Releases](#patch-releases)
- [Performing a Release](#performing-a-release)
- [Release Version Numbers](#release-version-numbers)
- [Release Candidates](#release-candidates)
## Release Philosophy
js-ipfs aims to have release every six weeks, two releases per quarter. During these 6 week releases, we go through 4 different stages that gives us the opportunity to test the new version against our test environments (unit, interop, integration), QA in our current production environment, IPFS apps (e.g. Desktop and WebUI) and with our community and _early testers_[1] that have IPFS running in production.
We might expand the six week release schedule in case of:
- No new updates to be added
- In case of a large community event that takes the core team availability away (e.g. IPFS Conf, Dev Meetings, IPFS Camp, etc.)
## Release Flow
js-ipfs releases come in 5 stages designed to gradually roll out changes and reduce the impact of any regressions that may have been introduced. If we need to merge non-trivial[2] changes during the process, we start over at stage 0.

### Stage 0 - Automated Testing
At this stage, we expect _all_ automated tests (unit, functional, integration, interop, testlab, performance, etc.) to pass.
### Stage 1 - Internal Testing
At this stage, we'll:
1. Start a partial-rollout to our own infrastructure.
2. Test against applications in the [ipfs](https://github.com/ipfs/) and [ipfs-shipyard](https://github.com/ipfs-shipyard/) organisations and a selection of other hand picked projects.
**Goals:**
1. Make sure we haven't introduced any obvious regressions.
2. Test the release in an environment we can monitor and easily roll back (i.e. our own infra).
### Stage 2 - Community Dev Testing
At this stage, we'll announce the impending release to the community and ask for pre-release testers.
**Goal:**
Test the release in as many non-production environments as possible. This is relatively low-risk but gives us a _breadth_ of testing internal testing can't.
### Stage 3 - Community Prod Testing
At this stage, we consider the release to be "production ready" and will ask the community and our early testers to (partially) deploy the release to their production infrastructure.
**Goals:**
1. Test the release in some production environments with heavy workloads.
2. Partially roll-out an upgrade to see how it affects the network.
3. Retain the ability to ship last-minute fixes before the final release.
### Stage 4 - Release
At this stage, the release is "battle hardened" and ready for wide deployment. A new version is published to npm, announcements are made and a blog post is published to [blog.ipfs.io](https://blog.ipfs.io).
## Release Cycle
A full release process should take about 3 weeks, a week per stage 1-3. We will start a new process every 6 weeks, regardless of when the previous release landed unless it's still ongoing.
### Patch Releases
If we encounter a serious bug in the stable latest release, we will create a patch release based on this release. For now, bug fixes will _not_ be backported to previous releases.
Patch releases will usually follow a compressed release cycle and should take 2-3 days. In a patch release:
1. Automated and internal testing (stage 0 and 1) will be compressed into a few hours - ideally less than a day.
2. Stage 2 will be skipped.
3. Community production testing will be shortened to 1-2 days of opt-in testing in production (early testers can choose to pass).
Some patch releases, especially ones fixing one or more complex bugs, may undergo the full release process.
## Performing a Release
The release is managed by the "Lead Maintainer" for js-ipfs. It starts with the opening of an issue containing the content available on the [RELEASE_ISSUE_TEMPLATE](./RELEASE_ISSUE_TEMPLATE.md) not more than **48 hours** after the previous release.
This issue is pinned and labeled ["release"](https://github.com/ipfs/js-ipfs/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Arelease). When the cycle is due to begin the 5 stages will be followed until the release is done.
## Release Version Numbers
js-ipfs is currently pre-1.0. In semver terms this means [anything may change at any time](https://semver.org/#spec-item-4).
However, pre-1.0 js-ipfs reserves MINOR version increments for BREAKING CHANGES _and_ feature additions and PATCH version increments for bug fixes.
Post `1.x.x` (future), MAJOR version number increments will contain BREAKING CHANGES, MINOR version increments will be reserved for backwards compatible new features and PATCH version increments for bug fixes.
We do not yet retroactively apply fixes to older releases (no Long Term Support releases for now), which means that we always recommend users to update to the latest, whenever possible.
### Release Candidates
Every commit to master results in the publishing of a Release Candidate. These are made available for users who want to try out the "bleeding edge" and can be installed using version numbers with the form `x.y.z-rc.n` where `x`, `y`, and `z` are the usual MAJOR, MINOR and PATCH version numbers and `n` (starting at 0) which is the number of commits to master since the last full release.
Alternatively the latest RC is tagged `next` on npm and can be installed using `npm install ipfs@next`.
---
- **[1]** - _early testers_ is an IPFS programme in which members of the community can self-volunteer to help test `js-ipfs` Release Candidates. You find more info about it at [EARLY_TESTERS.md](./EARLY_TESTERS.md)
- **[2]** - A non-trivial change is any change that could potentially introduce an issue that could not categorically be caught by automated testing. This is up to the discretion of the Lead Maintainer but the assumption is that every change is non-trivial unless proven otherwise.
================================================
FILE: docs/RELEASE_ISSUE_TEMPLATE.md
================================================
# Release Template
> short tl;dr; of the release
# 🗺 What's left for release
# 🚢 Estimated shipping date
# 🔦 Highlights
# 🏗 API Changes
# ✅ Release Checklist
- [ ] **Stage 0 - Automated Testing**
- [ ] Feature freeze. If any "non-trivial" changes (see the footnotes of [docs/releases.md](https://github.com/ipfs/js-ipfs/tree/master/docs/releases.md) for a definition) get added to the release, uncheck all the checkboxes and return to this stage.
- [ ] Automated Testing (already tested in CI) - Ensure that all tests are passing, this includes:
- [ ] unit/functional/integration/e2e
- [ ] interop
- [ ] ~~sharness~~ (Does not run `js-ipfs`)
- [ ] all the examples run without problems
- [ ] IPFS application testing
- [ ] ~~[webui](https://github.com/ipfs-shipyard/ipfs-webui)~~ (Does not depend on `js-ipfs` or `js-ipfs-http-client`)
- [ ] ~~[ipfs-desktop](https://github.com/ipfs-shipyard/ipfs-desktop)~~ (Does not depend on `js-ipfs` or `js-ipfs-http-client`)
- [ ] [ipfs-companion](https://github.com/ipfs-shipyard/ipfs-companion)
- [ ] [npm-on-ipfs](https://github.com/ipfs-shipyard/npm-on-ipfs)
- [ ] [peer-base](https://github.com/peer-base/peer-base)
- [ ] [service-worker-gateway](https://github.com/ipfs-shipyard/service-worker-gateway)
- [ ] Third party application testing
- [ ] [ipfs-log](https://github.com/orbitdb/ipfs-log)
- [ ] [orbit-db](https://github.com/orbitdb/orbit-db)
- [ ] [sidetree](https://github.com/decentralized-identity/sidetree)
- [ ] **Stage 1 - Internal Testing**
- [ ] Documentation
- [ ] Ensure that [README.md](https://github.com/ipfs/js-ipfs/tree/master/README.md) is up to date
- [ ] Install section
- [ ] API calls
- [ ] Packages Listing
- [ ] Publish a release candidate to npm
```sh
# All successful builds of master update the `build/last-successful` branch
# which contains an `npm-shrinkwrap.json`.
# This command checks that branch out, installs it's dependencies using `npm ci`,
# creates a release branch (e.g. release/v0.34.x), updates the minor prerelease
# version (e.g. 0.33.1 -> 0.34.0-rc.0) and publishes it to npm.
npx aegir publish-rc
# Later we may wish to update the rc. First cherry-pick/otherwise merge the
# new commits into the release branch on github (e.g. not locally) and wait
# for CI to pass. Then update the lockfiles used by CI (n.b. one day this
# will be done by our ci tools) with this command:
npx aegir update-release-branch-lockfiles release/v0.34.x
# Then update the rc published on npm. This command pulls the specified
# release branch, installs it's dependencies `npm ci`, increments the
# prerelease version (e.g. 0.34.0-rc.0 -> 0.34.0-rc.1) and publishes it
# to npm.
npx aegir update-rc release/v0.34.x
```
- Network Testing:
- test lab things - TBD
- Infrastructure Testing:
- TBD
- [ ] **Stage 2 - Community Dev Testing**
- [ ] Reach out to the IPFS _early testers_ listed in [docs/EARLY_TESTERS.md](https://github.com/ipfs/js-ipfs/tree/master/docs/EARLY_TESTERS.md) for testing this release (check when no more problems have been reported). If you'd like to be added to this list, please file a PR.
- [ ] Reach out on IRC for additional early testers.
- [ ] **Stage 3 - Community Prod Testing**
- [ ] Update [js.ipfs.io](https://js.ipfs.io) examples to use the latest js-ipfs
- [ ] Invite the IPFS [_early testers_](https://github.com/ipfs/js-ipfs/tree/master/docs/EARLY_TESTERS.md) to deploy the release to part of their production infrastructure.
- [ ] Invite the wider community (link to the release issue):
- [ ] [discuss.ipfs.io](https://discuss.ipfs.io/c/announcements)
- [ ] Twitter
- [ ] IRC
- [ ] **Stage 4 - Release**
- [ ] Take a snapshot of everyone that has contributed to this release (including its direct dependencies in IPFS, libp2p, IPLD and multiformats) using [the js-ipfs-contributors module](https://www.npmjs.com/package/js-ipfs-contributors).
- [ ] Publish to npm:
```sh
git checkout release/v0.34.x
# Re-install dependencies using lockfile (will automatically remove your
# node_modules folder) (Ensures the versions used for the browser build are the
# same that have been verified by CI)
npm ci
# lint, build, test, tag, publish
npm run release-minor
# reintegrate release branch into master
git rm npm-shrinkwrap.json yarn.lock
git commit -m 'chore: removed lock files'
git checkout master
git merge release/v0.34.x
git push
```
- [ ] Publish a blog post to [github.com/ipfs/blog](https://github.com/ipfs/blog) (at minimum, a c&p of this release issue with all the highlights, API changes and thank yous)
- [ ] Broadcasting (link to blog post)
- [ ] Twitter
- [ ] IRC
- [ ] [Reddit](https://reddit.com/r/ipfs)
- [ ] [discuss.ipfs.io](https://discuss.ipfs.io/c/announcements)
- [ ] Announce it on the [IPFS Users Mailing List](https://groups.google.com/forum/#!forum/ipfs-users)
- [ ] Copy release notes to the [GitHub Release description](https://github.com/ipfs/js-ipfs/releases)
# ❤️ Huge thank you to everyone that made this release possible
# 🙌🏽 Want to contribute?
Would you like to contribute to the IPFS project and don't know how? Well, there are a few places you can get started:
- Check the issues with the `help wanted` label in the [js-ipfs repo](https://github.com/ipfs/js-ipfs/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)
- Join an IPFS All Hands, introduce yourself and let us know where you would like to contribute - https://github.com/ipfs/team-mgmt/#weekly-ipfs-all-hands
- Hack with IPFS and show us what you made! The All Hands call is also the perfect venue for demos, join in and show us what you built
- Join the discussion at https://discuss.ipfs.io/ and help users finding their answers.
- Join the [🚀 IPFS Core Implementations Weekly Sync 🛰](https://github.com/ipfs/team-mgmt/issues/992) and be part of the action!
# ⁉️ Do you have questions?
The best place to ask your questions about IPFS, how it works and what you can do with it is at [discuss.ipfs.io](https://discuss.ipfs.io). We are also available at the `#ipfs` channel on Freenode.
================================================
FILE: docs/core-api/BITSWAP.md
================================================
# Bitswap API
- [`ipfs.bitswap.wantlist([options])`](#ipfsbitswapwantlistoptions)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.bitswap.wantlistForPeer(peerId, [options])`](#ipfsbitswapwantlistforpeerpeerid-options)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [`ipfs.bitswap.unwant(cids, [options])`](#ipfsbitswapunwantcids-options)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-2)
- [Example](#example-2)
- [`ipfs.bitswap.stat([options])`](#ipfsbitswapstatoptions)
- [Parameters](#parameters-3)
- [Options](#options-3)
- [Returns](#returns-3)
- [Example](#example-3)
## `ipfs.bitswap.wantlist([options])`
> Returns the wantlist for your node
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An array of [CID][]s currently in the wantlist |
### Example
```JavaScript
const list = await ipfs.bitswap.wantlist()
console.log(list)
// [ CID('QmHash') ]
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.bitswap.wantlistForPeer(peerId, [options])`
> Returns the wantlist for a connected peer
### Parameters
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| peerId | [PeerId][] | A peer ID to return the wantlist for |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An array of [CID][]s currently in the wantlist |
### Example
```JavaScript
const list = await ipfs.bitswap.wantlistForPeer(peerId)
console.log(list)
// [ CID('QmHash') ]
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.bitswap.unwant(cids, [options])`
> Removes one or more CIDs from the wantlist
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cids | A [CID][] or Array of [CID][]s | The CIDs to remove from the wantlist |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | A promise that resolves once the request is complete |
### Example
```JavaScript
let list = await ipfs.bitswap.wantlist()
console.log(list)
// [ CID('QmHash') ]
await ipfs.bitswap.unwant(cid)
list = await ipfs.bitswap.wantlist()
console.log(list)
// []
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.bitswap.stat([options])`
> Show diagnostic information on the bitswap agent.
Note: `bitswap.stat` and `stats.bitswap` can be used interchangeably.
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object that contains information about the bitswap agent |
The returned object contains the following keys:
- `provideBufLen` is an integer.
- `wantlist` (array of [CID][cid]s)
- `peers` (array of [PeerId][peerId]s)
- `blocksReceived` is a [BigInt][1]
- `dataReceived` is a [BigInt][1]
- `blocksSent` is a [BigInt][1]
- `dataSent` is a [BigInt][1]
- `dupBlksReceived` is a [BigInt][1]
- `dupDataReceived` is a [BigInt][1]
### Example
```JavaScript
const stats = await ipfs.bitswap.stat()
console.log(stats)
// {
// provideBufLen: 0,
// wantlist: [ CID('QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM') ],
// peers:
// [ 'QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM',
// 'QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu',
// 'QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd' ],
// blocksReceived: 0,
// dataReceived: 0,
// blocksSent: 0,
// dataSent: 0,
// dupBlksReceived: 0,
// dupDataReceived: 0
// }
```
A great source of [examples][] can be found in the tests for this API.
[1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/bitswap
[cid]: https://docs.ipfs.io/concepts/content-addressing
[peerid]: https://docs.libp2p.io/concepts/peer-id/
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
================================================
FILE: docs/core-api/BLOCK.md
================================================
# Block API
- [`ipfs.block.get(cid, [options])`](#ipfsblockgetcid-options)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.block.put(block, [options])`](#ipfsblockputblock-options)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [`ipfs.block.rm(cid, [options])`](#ipfsblockrmcid-options)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-2)
- [Example](#example-2)
- [`ipfs.block.stat(cid, [options])`](#ipfsblockstatcid-options)
- [Parameters](#parameters-3)
- [Options](#options-3)
- [Returns](#returns-3)
- [Example](#example-3)
## `ipfs.block.get(cid, [options])`
> Get a raw IPFS block.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][], `String` or `Uint8Array` | A CID that corresponds to the desired block |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
| preload | `boolean` | `false` | Whether to preload all blocks created during this operation |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | A Uint8Array containing the data of the block |
### Example
```JavaScript
const block = await ipfs.block.get(cid)
console.log(block)
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.block.put(block, [options])`
> Stores input as an IPFS block.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| block | `Uint8Array` | The block of data to store |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| format | `String` | `'dag-pb'` | The codec to use to create the CID |
| mhtype | `String` | `sha2-256` | The hashing algorithm to use to create the CID |
| mhlen | `Number` | `undefined` | The hash length (only relevant for `go-ipfs`) |
| version | `Number` | `0` | The version to use to create the CID |
| pin | `boolean` | `false` | If true, pin added blocks recursively |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
| preload | `boolean` | `false` | Whether to preload all blocks created during this operation |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | A [CID][CID] type object containing the hash of the block |
### Example
```JavaScript
// Defaults
const buf = new TextEncoder().encode('a serialized object')
const decoder = new TextDecoder()
const block = await ipfs.block.put(buf)
console.log(decoder.decode(block.data))
// Logs:
// a serialized object
console.log(block.cid.toString())
// Logs:
// the CID of the object
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.block.rm(cid, [options])`
> Remove one or more IPFS block(s).
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | A [CID][] or Array of [CID][]s | Blocks corresponding to the passed CID(s) will be removed |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| force | `boolean` | `false` | Ignores nonexistent blocks |
| quiet | `boolean` | `false` | Write minimal output |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields objects containing hash and (potentially) error strings |
Each object yielded is of the form:
```js
{
cid: CID,
error?: Error
}
```
Note: If an error is present for a given object, the block with that cid was not removed and the `error` will contain the reason why, for example if the block was pinned.
### Example
```JavaScript
for await (const result of ipfs.block.rm(cid)) {
if (result.error) {
console.error(`Failed to remove block ${result.cid} due to ${result.error.message}`)
} else {
console.log(`Removed block ${result.cid}`)
}
}
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.block.stat(cid, [options])`
> Print information of a raw IPFS block.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | A [CID][] or Array of [CID][]s | The stats of the passed CID will be returned |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
| preload | `boolean` | `false` | Whether to preload all blocks created during this operation |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object containing the block's info |
the returned object has the following keys:
```JavaScript
{
cid: CID
size: number
}
```
### Example
```JavaScript
const multihashStr = 'QmQULBtTjNcMwMr4VMNknnVv3RpytrLSdgpvMcTnfNhrBJ'
const cid = CID.parse(multihashStr)
const stats = await ipfs.block.stat(cid)
console.log(stats.cid.toString())
// Logs: QmQULBtTjNcMwMr4VMNknnVv3RpytrLSdgpvMcTnfNhrBJ
console.log(stat.size)
// Logs: 3739
```
A great source of [examples][] can be found in the tests for this API.
[block]: https://github.com/ipfs/js-ipfs-block
[multihash]: https://github.com/multiformats/multihash
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/block
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
[cid]: https://docs.ipfs.io/concepts/content-addressing
================================================
FILE: docs/core-api/BOOTSTRAP.md
================================================
# Bootstrap API
> Manipulates the bootstrap list, which contains the addresses of the bootstrap nodes. These are the trusted peers from which to learn about other peers in the network.
Warning: your node requires bootstrappers to join the network and find other peers.
If you edit this list, you may find you have reduced or no connectivity. If this is the case, please reset your node's bootstrapper list with `ipfs.bootstrap.reset()`.
- [`ipfs.bootstrap.add(addr, [options])`](#ipfsbootstrapaddaddr-options)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.bootstrap.reset([options])`](#ipfsbootstrapresetoptions)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [`ipfs.bootstrap.list([options])`](#ipfsbootstraplistoptions)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-2)
- [Example](#example-2)
- [`ipfs.bootstrap.rm(addr, [options])`](#ipfsbootstraprmaddr-options)
- [Parameters](#parameters-3)
- [Options](#options-3)
- [Returns](#returns-3)
- [Example](#example-3)
- [`ipfs.bootstrap.clear([options])`](#ipfsbootstrapclearoptions)
- [Parameters](#parameters-4)
- [Options](#options-4)
- [Returns](#returns-4)
- [Example](#example-4)
## `ipfs.bootstrap.add(addr, [options])`
> Add a peer address to the bootstrap list
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| addr | [MultiAddr][] | The address of a network peer |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise<{ Peers: Array }>` | An object that contains an array with all the added addresses |
example of the returned object:
```JavaScript
{
Peers: [address1, address2, ...]
}
```
### Example
```JavaScript
const validIp4 = '/ip4/104....9z'
const res = await ipfs.bootstrap.add(validIp4)
console.log(res.Peers)
// Logs:
// ['/ip4/104....9z']
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.bootstrap.reset([options])`
> Reset the bootstrap list to contain only the default bootstrap nodes
### Parameters
None.
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise<{ Peers: Array }>` | An object that contains an array with all the added addresses |
example of the returned object:
```JavaScript
{
Peers: [address1, address2, ...]
}
```
### Example
```JavaScript
const res = await ipfs.bootstrap.reset()
console.log(res.Peers)
// Logs:
// ['/ip4/104....9z']
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.bootstrap.list([options])`
> List all peer addresses in the bootstrap list
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise<{ Peers: Array }>` | An object that contains an array with all the bootstrap addresses |
example of the returned object:
```JavaScript
{
Peers: [address1, address2, ...]
}
```
### Example
```JavaScript
const res = await ipfs.bootstrap.list()
console.log(res.Peers)
// Logs:
// [address1, address2, ...]
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.bootstrap.rm(addr, [options])`
> Remove a peer address from the bootstrap list
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| addr | [MultiAddr][] | The address of a network peer |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise<{ Peers: Array }>` | An object that contains an array with all the removed addresses |
```JavaScript
{
Peers: [address1, address2, ...]
}
```
### Example
```JavaScript
const res = await ipfs.bootstrap.rm('address1')
console.log(res.Peers)
// Logs:
// [address1, ...]
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.bootstrap.clear([options])`
> Remove all peer addresses from the bootstrap list
### Parameters
None.
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise<{ Peers: Array }>` | An object that contains an array with all the removed addresses |
```JavaScript
{
Peers: [address1, address2, ...]
}
```
### Example
```JavaScript
const res = await ipfs.bootstrap.clear()
console.log(res.Peers)
// Logs:
// [address1, address2, ...]
```
A great source of [examples][] can be found in the tests for this API.
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/bootstrap
[MultiAddr]: https://github.com/multiformats/js-multiaddr
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
================================================
FILE: docs/core-api/CONFIG.md
================================================
# Config API
- [`ipfs.config.get(key, [options])`](#ipfsconfiggetkey-options)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.config.getAll([options])`](#ipfsconfiggetkey-options)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.config.set(key, value, [options])`](#ipfsconfigsetkey-value-options)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [`ipfs.config.replace(config, [options])`](#ipfsconfigreplaceconfig-options)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-2)
- [Example](#example-2)
- [`ipfs.config.profiles.list([options])`](#ipfsconfigprofileslistoptions)
- [Parameters](#parameters-3)
- [Options](#options-3)
- [Returns](#returns-3)
- [Example](#example-3)
- [`ipfs.config.profiles.apply(name, [options])`](#ipfsconfigprofilesapplyname-options)
- [Parameters](#parameters-4)
- [Options](#options-4)
- [Returns](#returns-4)
- [Example](#example-4)
## `ipfs.config.get(key, [options])`
> Returns the currently being used config. If the daemon is off, it returns the stored config.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| key | `String` | The key of the value that should be fetched from the config file. |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object containing the configuration of the IPFS node |
### Example
```JavaScript
const config = await ipfs.config.get()
console.log(config)
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.config.getAll([options])`
> Returns the full config been used. If the daemon is off, it returns the stored config.
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object containing the configuration of the IPFS node |
### Example
```JavaScript
const config = await ipfs.config.getAll()
console.log(config)
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.config.set(key, value, [options])`
> Adds or replaces a config value.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| key | `String` | The key of the value that should be added or replaced |
| value | any | The value to be set |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
Note that this operation will **not** spark the restart of any service, i.e: if a config.replace changes the multiaddrs of the Swarm, Swarm will have to be restarted manually for the changes to take difference.
### Example
```JavaScript
await ipfs.config.set('Discovery.MDNS.Enabled', false)
// MDNS Discovery was set to false
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.config.replace(config, [options])`
> Adds or replaces a config file
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| config | Object | An object that contains the new config |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
Note that this operation will **not** spark the restart of any service, i.e: if a config.replace changes the multiaddrs of the Swarm, Swarm will have to be restarted manually for the changes to take difference.
### Example
```JavaScript
const newConfig = {
Bootstrap: []
}
await ipfs.config.replace(newConfig)
// config has been replaced
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.config.profiles.list([options])`
> List available config profiles
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An array with all the available config profiles |
### Example
```JavaScript
const profiles = await ipfs.config.profiles.list()
profiles.forEach(profile => {
console.info(profile.name, profile.description)
})
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.config.profiles.apply(name, [options])`
> Apply a config profile
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| name | `String` | The name of the profile to apply |
Call `config.profiles.list()` for a list of valid profile names.
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| dryRun | `boolean` | false | If true does not apply the profile |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object containing both the `original` and `updated` config |
### Example
```JavaScript
const diff = await ipfs.config.profiles.apply('lowpower')
console.info(diff.original)
console.info(diff.updated)
```
Note that you will need to restart your node for config changes to take effect.
A great source of [examples][] can be found in the tests for this API.
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/config
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
================================================
FILE: docs/core-api/DAG.md
================================================
# DAG API
> The dag API comes to replace the `object API`, it supports the creation and manipulation of dag-pb object, as well as other IPLD formats (i.e dag-cbor, ethereum-block, git, etc)
- [`ipfs.dag.export(cid, [options])`](#ipfsdagexportcid-options)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.dag.put(dagNode, [options])`](#ipfsdagputdagnode-options)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [`ipfs.dag.get(cid, [options])`](#ipfsdaggetcid-options)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-2)
- [Example](#example-2)
- [`ipfs.dag.import(source, [options])`](#ipfsdagimportsource-options)
- [Parameters](#parameters-3)
- [Options](#options-3)
- [Returns](#returns-3)
- [Example](#example-3)
- [`ipfs.dag.resolve(ipfsPath, [options])`](#ipfsdagresolveipfspath-options)
- [Parameters](#parameters-4)
- [Options](#options-4)
- [Returns](#returns-4)
- [Example](#example-4)
_Explore the DAG API through interactive coding challenges in our ProtoSchool tutorials:_
- _[P2P data links with content addressing](https://proto.school/#/basics/) (beginner)_
- _[Blogging on the Decentralized Web](https://proto.school/#/blog/) (intermediate)_
## `ipfs.dag.export(cid, [options])`
> Returns a stream of Uint8Arrays that make up a [CAR file][]
Exports a CAR for the entire DAG available from the given root CID. The CAR will have a single
root and IPFS will attempt to fetch and bundle all blocks that are linked within the connected
DAG.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][] | The root CID of the DAG we wish to export |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | A stream containing the car file bytes |
### Example
```JavaScript
import { Readable } from 'stream'
const out = await ipfs.dag.export(cid)
Readable.from(out).pipe(fs.createWriteStream('example.car'))
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.dag.put(dagNode, [options])`
> Store an IPLD format node
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| dagNode | `Object` | A DAG node that follows one of the supported IPLD formats |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| storeCodec | `String` | `'dag-cbor'` | The codec that the stored object will be encoded with |
| inputCodec | `String` | `undefined` | If an already encoded object is provided (as a `Uint8Array`), the codec that the object is encoded with, otherwise it is assumed the `dagNode` argument is an object to be encoded |
| hashAlg | `String` | `'sha2-256'` | The hash algorithm to be used over the serialized DAG node |
| cid | [CID][] | `'dag-cbor'` | The IPLD format multicodec |
| pin | `boolean` | `false` | Pin this node when adding to the blockstore |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
**Note**: You should pass `cid` or the `format` & `hashAlg` pair but _not both_.
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | A [CID][] instance. The CID generated through the process or the one that was passed |
### Example
```JavaScript
const obj = { simple: 'object' }
const cid = await ipfs.dag.put(obj, { storeCodec: 'dag-cbor', hashAlg: 'sha2-512' })
console.log(cid.toString())
// zBwWX9ecx5F4X54WAjmFLErnBT6ByfNxStr5ovowTL7AhaUR98RWvXPS1V3HqV1qs3r5Ec5ocv7eCdbqYQREXNUfYNuKG
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.dag.get(cid, [options])`
> Retrieve an IPLD format node
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][] | A CID that resolves to a node to get |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| path | `String` | An optional path within the DAG to resolve |
| localResolve | `boolean` | `false` | If set to true, it will avoid resolving through different objects |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object representing an IPLD format node |
The returned object contains:
- `value` - the value or node that was fetched during the get operation.
- `remainderPath` - The remainder of the Path that the node was unable to resolve or what was left in a localResolve scenario.
### Example
```JavaScript
// example obj
const obj = {
a: 1,
b: [1, 2, 3],
c: {
ca: [5, 6, 7],
cb: 'foo'
}
}
const cid = await ipfs.dag.put(obj, { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' })
console.log(cid.toString())
// zdpuAmtur968yprkhG9N5Zxn6MFVoqAWBbhUAkNLJs2UtkTq5
async function getAndLog(cid, path) {
const result = await ipfs.dag.get(cid, { path })
console.log(result.value)
}
await getAndLog(cid, '/a')
// Logs:
// 1
await getAndLog(cid, '/b')
// Logs:
// [1, 2, 3]
await getAndLog(cid, '/c')
// Logs:
// {
// ca: [5, 6, 7],
// cb: 'foo'
// }
await getAndLog(cid, '/c/ca/1')
// Logs:
// 6
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.dag.import(source, [options])`
> Adds one or more [CAR file][]s full of blocks to the repo for this node
Import all blocks from one or more CARs and optionally recursively pin the roots identified
within the CARs.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| sources | `AsyncIterable` | One or more [CAR file][] streams |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| pinRoots | `boolean` | `true` | Whether to recursively pin each root to the blockstore |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable<{ root: { cid: CID, pinErrorMsg?: string } }>` | A stream containing all roots from the car file(s) that are pinned |
### Example
```JavaScript
import fs from 'fs'
for await (const result of ipfs.dag.import(fs.createReadStream('./path/to/archive.car'))) {
console.info(result)
// Qmfoo
}
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.dag.resolve(ipfsPath, [options])`
> Returns the CID and remaining path of the node at the end of the passed IPFS path
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| ipfsPath | `String` or [CID][] | An IPFS path, e.g. `/ipfs/bafy/dir/file.txt` or a [CID][] instance |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| path | `String` | `undefined` | If `ipfsPath` is a [CID][], you may pass a path here |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise<{ cid: CID, remainderPath: String }>` | The last CID encountered during the traversal and the path to the end of the IPFS path inside the node referenced by the CID |
### Example
```JavaScript
// example obj
const obj = {
a: 1,
b: [1, 2, 3],
c: {
ca: [5, 6, 7],
cb: 'foo'
}
}
const cid = await ipfs.dag.put(obj, { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' })
console.log(cid.toString())
// bafyreicyer3d34cutdzlsbe2nqu5ye62mesuhwkcnl2ypdwpccrsecfmjq
const result = await ipfs.dag.resolve(`${cid}/c/cb`)
console.log(result)
// Logs:
// {
// cid: CID(bafyreicyer3d34cutdzlsbe2nqu5ye62mesuhwkcnl2ypdwpccrsecfmjq),
// remainderPath: 'c/cb'
// }
```
A great source of [examples][] can be found in the tests for this API.
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/dag
[cid]: https://docs.ipfs.io/concepts/content-addressing
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
[CAR file]: https://ipld.io/specs/transport/car/
================================================
FILE: docs/core-api/DHT.md
================================================
# DHT API
- [`ipfs.dht.findPeer(peerId, [options])`](#ipfsdhtfindpeerpeerid-options)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.dht.findProvs(cid, [options])`](#ipfsdhtfindprovscid-options)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [`ipfs.dht.get(key, [options])`](#ipfsdhtgetkey-options)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-2)
- [Example](#example-2)
- [`ipfs.dht.provide(cid, [options])`](#ipfsdhtprovidecid-options)
- [Parameters](#parameters-3)
- [Options](#options-3)
- [Returns](#returns-3)
- [Example](#example-3)
- [`ipfs.dht.put(key, value, [options])`](#ipfsdhtputkey-value-options)
- [Parameters](#parameters-4)
- [Options](#options-4)
- [Returns](#returns-4)
- [Example](#example-4)
- [`ipfs.dht.query(peerId, [options])`](#ipfsdhtquerypeerid-options)
- [Parameters](#parameters-5)
- [Options](#options-5)
- [Returns](#returns-5)
- [Example](#example-5)
## `ipfs.dht.findPeer(peerId, [options])`
> Find the multiaddresses associated with a Peer ID
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| peerId | [PeerID][] | The Peer ID of the node to find |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise<{ id: String, addrs: Multiaddr[] }>` | A promise that resolves to an object with `id` and `addrs`. `id` is a String - the peer's ID and `addrs` is an array of [Multiaddr](https://github.com/multiformats/js-multiaddr/) - addresses for the peer. |
### Example
```JavaScript
const info = await ipfs.dht.findPeer('QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt')
console.log(info.id.toString())
/*
QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt
*/
info.addrs.forEach(addr => console.log(addr.toString()))
/*
/ip4/147.75.94.115/udp/4001/quic
/ip6/2604:1380:3000:1f00::1/udp/4001/quic
/dnsaddr/bootstrap.libp2p.io
/ip6/2604:1380:3000:1f00::1/tcp/4001
/ip4/147.75.94.115/tcp/4001
*/
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.dht.findProvs(cid, [options])`
> Find peers that can provide a specific value, given a CID.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][] | The CID of the content to find |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| numProviders | `Number` | 20 | How many providers to find |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
Note that if `options.numProviders` are not found an error will be thrown.
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable<{ id: String, addrs: Multiaddr[] }>` | A async iterable that yields objects with `id` and `addrs`. `id` is a String - the peer's ID and `addrs` is an array of [Multiaddr](https://github.com/multiformats/js-multiaddr/) - addresses for the peer. |
### Example
```JavaScript
import { CID } from 'multiformats/cid'
const providers = ipfs.dht.findProvs(CID.parse('QmdPAhQRxrDKqkGPvQzBvjYe3kU8kiEEAd2J6ETEamKAD9'))
for await (const provider of providers) {
console.log(provider.id.toString())
}
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.dht.get(key, [options])`
> Given a key, query the routing system for its best value.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| key | `Uint8Array` or `string` | The key associated with the value to find |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | The value that was stored under that key |
### Example
```JavaScript
const value = await ipfs.dht.get(key)
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.dht.provide(cid, [options])`
> Announce to the network that you are providing given values.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][] or Array<[CID][]> | The key associated with the value to find |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| recursive | `boolean` | false | If `true` the entire graph will be provided recursively |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | DHT query messages. See example below for structure. |
Note: You must consume the iterable to completion to complete the provide operation.
### Example
```JavaScript
for await (const message of ipfs.dht.provide('QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR')) {
console.log(message)
}
/*
Prints objects like:
{
extra: 'dial backoff',
id: PeerId('QmWtewmnzJiQevJPSmG9s8aC7yRfK2WXTCdRc1pCbDFu6z'),
responses: [
{
addrs: [
Multiaddr(/ip4/127.0.0.1/tcp/4001),
Multiaddr(/ip4/172.20.0.3/tcp/4001),
Multiaddr(/ip4/35.178.190.196/tcp/1024)
],
id: PeerId('QmRz5Nth4jTFuJJKcjyb6uwvrhxWbruRvamKY2PJxwJKw8')
}
],
type: 1
}
For message `type` values, see:
https://github.com/libp2p/go-libp2p-core/blob/6e566d10f4a5447317a66d64c7459954b969bdab/routing/query.go#L15-L24
*/
```
Alternatively you can simply "drain" the iterable:
```js
import drain from 'it-drain'
await drain(ipfs.dht.provide('QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'))
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.dht.put(key, value, [options])`
> Write a key/value pair to the routing system.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| key | Uint8Array | The key to put the value as |
| value | Uint8Array | Value to put |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | DHT query messages. See example below for structure. |
### Example
```JavaScript
for await (const message of ipfs.dht.put(key, value)) {
console.log(message)
}
/*
Prints objects like:
{
extra: 'dial backoff',
id: PeerId('QmWtewmnzJiQevJPSmG9s8aC7yRfK2WXTCdRc1pCbDFu6z'),
responses: [
{
addrs: [
Multiaddr(/ip4/127.0.0.1/tcp/4001),
Multiaddr(/ip4/172.20.0.3/tcp/4001),
Multiaddr(/ip4/35.178.190.196/tcp/1024)
],
id: PeerId('QmRz5Nth4jTFuJJKcjyb6uwvrhxWbruRvamKY2PJxwJKw8')
}
],
type: 1
}
For message `type` values, see:
https://github.com/libp2p/go-libp2p-core/blob/6e566d10f4a5447317a66d64c7459954b969bdab/routing/query.go#L15-L24
*/
```
Alternatively you can simply "drain" the iterable:
```js
import drain from 'it-drain'
await drain(ipfs.dht.put(key, value))
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.dht.query(peerId, [options])`
> Find the closest Peer IDs to a given Peer ID or CID by querying the DHT.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| peerId | [PeerID][] or [CID][] | The peer id to query |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | DHT query messages. See example below for structure. |
### Example
```JavaScript
for await (const info of ipfs.dht.query('QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt')) {
console.log(info)
}
/*
Prints objects like:
{
extra: 'dial backoff',
id: 'QmWtewmnzJiQevJPSmG9s8aC7yRfK2WXTCdRc1pCbDFu6z',
responses: [
{
addrs: [
Multiaddr(/ip4/127.0.0.1/tcp/4001),
Multiaddr(/ip4/172.20.0.3/tcp/4001),
Multiaddr(/ip4/35.178.190.196/tcp/1024)
],
id: PeerId('QmRz5Nth4jTFuJJKcjyb6uwvrhxWbruRvamKY2PJxwJKw8')
}
],
type: 1
}
For message `type` values, see:
https://github.com/libp2p/go-libp2p-core/blob/6e566d10f4a5447317a66d64c7459954b969bdab/routing/query.go#L15-L24
*/
```
A great source of [examples][] can be found in the tests for this API.
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/dht
[peerid]: https://docs.libp2p.io/concepts/peer-id/
[cid]: https://docs.ipfs.io/concepts/content-addressing
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
================================================
FILE: docs/core-api/FILES.md
================================================
# Files API
> The files API enables users to use the File System abstraction of IPFS. There are two Files API, one at the top level, the original `add`, `cat`, `get` and `ls`, and another behind the [`files`, also known as MFS](https://docs.ipfs.io/guides/concepts/mfs/)
_Explore the Mutable File System through interactive coding challenges in our [ProtoSchool tutorial](https://proto.school/#/mutable-file-system/)._
- [The Regular API](#the-regular-api)
- [`ipfs.add(data, [options])`](#ipfsadddata-options)
- [Parameters](#parameters)
- [FileObject](#fileobject)
- [FileContent](#filecontent)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.addAll(source, [options])`](#ipfsaddallsource-options)
- [Parameters](#parameters-1)
- [FileStream](#filestream)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [Notes](#notes)
- [Chunking options](#chunking-options)
- [Hash algorithms](#hash-algorithms)
- [Importing files from the file system](#importing-files-from-the-file-system)
- [Importing a file from a URL](#importing-a-file-from-a-url)
- [`ipfs.cat(ipfsPath, [options])`](#ipfscatipfspath-options)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-2)
- [Example](#example-2)
- [`ipfs.get(ipfsPath, [options])`](#ipfsgetipfspath-options)
- [Parameters](#parameters-3)
- [Options](#options-3)
- [Returns](#returns-3)
- [Example](#example-3)
- [`ipfs.ls(ipfsPath)`](#ipfslsipfspath)
- [Parameters](#parameters-4)
- [Options](#options-4)
- [Returns](#returns-4)
- [Example](#example-4)
- [The Mutable Files API](#the-mutable-files-api)
- [`ipfs.files.chmod(path, mode, [options])`](#ipfsfileschmodpath-mode-options)
- [Parameters](#parameters-5)
- [Options](#options-5)
- [Returns](#returns-5)
- [Example](#example-5)
- [`ipfs.files.cp(...from, to, [options])`](#ipfsfilescpfrom-to-options)
- [Parameters](#parameters-6)
- [Options](#options-6)
- [Returns](#returns-6)
- [Example](#example-6)
- [Notes](#notes-1)
- [`ipfs.files.mkdir(path, [options])`](#ipfsfilesmkdirpath-options)
- [Parameters](#parameters-7)
- [Options](#options-7)
- [Returns](#returns-7)
- [Example](#example-7)
- [`ipfs.files.stat(path, [options])`](#ipfsfilesstatpath-options)
- [Parameters](#parameters-8)
- [Options](#options-8)
- [Returns](#returns-8)
- [Example](#example-8)
- [`ipfs.files.touch(path, [options])`](#ipfsfilestouchpath-options)
- [Parameters](#parameters-9)
- [Options](#options-9)
- [Returns](#returns-9)
- [Example](#example-9)
- [`ipfs.files.rm(path, [options])`](#ipfsfilesrmpath-options)
- [Parameters](#parameters-10)
- [Options](#options-10)
- [Returns](#returns-10)
- [Example](#example-10)
- [`ipfs.files.read(path, [options])`](#ipfsfilesreadpath-options)
- [Parameters](#parameters-11)
- [Options](#options-11)
- [Returns](#returns-11)
- [Example](#example-11)
- [`ipfs.files.write(path, content, [options])`](#ipfsfileswritepath-content-options)
- [Parameters](#parameters-12)
- [Options](#options-12)
- [Returns](#returns-12)
- [Example](#example-12)
- [`ipfs.files.mv(...from, to, [options])`](#ipfsfilesmvfrom-to-options)
- [Parameters](#parameters-13)
- [Options](#options-13)
- [Returns](#returns-13)
- [Example](#example-13)
- [Notes](#notes-2)
- [`ipfs.files.flush(path, [options])`](#ipfsfilesflushpath-options)
- [Parameters](#parameters-14)
- [Options](#options-14)
- [Returns](#returns-14)
- [Example](#example-14)
- [`ipfs.files.ls(path, [options])`](#ipfsfileslspath-options)
- [Parameters](#parameters-15)
- [Options](#options-15)
- [Returns](#returns-15)
- [Example](#example-15)
## The Regular API
The regular, top-level API for add, cat, get and ls Files on IPFS
### `ipfs.add(data, [options])`
> Import a file or data into IPFS.
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| data | Object | Data to import (see below) |
`data` may be:
* `FileContent` (see below for definition)
* `FileObject` (see below for definition)
##### FileObject
`FileObject` is a plain JS object of the following form:
```js
{
// The path you want the file to be accessible at from the root CID _after_ it has been added
path?: string
// The contents of the file (see below for definition)
content?: FileContent
// File mode to store the entry with (see https://en.wikipedia.org/wiki/File_system_permissions#Numeric_notation)
mode?: number | string
// The modification time of the entry (see below for definition)
mtime?: UnixTime
}
```
If no `path` is specified, then the item will be added to the root level and will be given a name according to it's CID.
If no `content` is passed, then the item is treated as an empty directory.
One of `path` or `content` _must_ be passed.
Both `mode` and `mtime` are optional and will result in different [CID][]s for the same file if passed.
`mode` will have a default value applied if not set, see [UnixFS Metadata](https://github.com/ipfs/specs/blob/master/UNIXFS.md#metadata) for further discussion.
##### FileContent
`FileContent` is one of the following types:
```js
Uint8Array | Blob | String | Iterable | Iterable | AsyncIterable | ReadableStream
```
`UnixTime` is one of the following types:
```js
Date | { secs: number, nsecs?: number } | number[]
```
As an object, `secs` is the number of seconds since (positive) or before (negative) the Unix Epoch began and `nsecs` is the number of nanoseconds since the last full second.
As an array of numbers, it must have two elements, as per the output of [`process.hrtime()`](https://nodejs.org/dist/latest/docs/api/process.html#process_process_hrtime_time).
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| chunker | `String` | `'size-262144'` | chunking algorithm used to build ipfs DAGs |
| cidVersion | `Number` | `0` | the CID version to use when storing the data |
| hashAlg | `String` | `'sha2-256'` | multihash hashing algorithm to use |
| onlyHash | `boolean` | `false` | If true, will not add blocks to the blockstore |
| pin | `boolean` | `true` | pin this object when adding |
| progress | function | `undefined` | a function that will be called with the number of bytes added as a file is added to ipfs and the path of the file being added |
| rawLeaves | `boolean` | `false` | if true, DAG leaves will contain raw file data and not be wrapped in a protobuf |
| trickle | `boolean` | `false` | if true will use the [trickle DAG](https://godoc.org/github.com/ipsn/go-ipfs/gxlibs/github.com/ipfs/go-unixfs/importer/trickle) format for DAG generation |
| wrapWithDirectory | `boolean` | `false` | Adds a wrapping node around the content |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | A object describing the added data |
Each yielded object is of the form:
```JavaScript
{
path: '/tmp/myfile.txt',
cid: CID('QmHash'),
mode: Number, // implicit if not provided - 0644 for files, 0755 for directories
mtime?: { secs: Number, nsecs: Number },
size: 123
}
```
#### Example
```js
const file = {
path: '/tmp/myfile.txt',
content: 'ABC'
}
const result = await ipfs.add(file)
console.info(result)
/*
Prints:
{
"path": "tmp",
"cid": CID("QmWXdjNC362aPDtwHPUE9o2VMqPeNeCQuTBTv1NsKtwypg"),
"mode": 493,
"mtime": { secs: Number, nsecs: Number },
"size": 67
}
*/
```
Now [ipfs.io/ipfs/Qm..pg/myfile.txt](https://ipfs.io/ipfs/QmWXdjNC362aPDtwHPUE9o2VMqPeNeCQuTBTv1NsKtwypg/myfile.txt) returns the "ABC" string.
### `ipfs.addAll(source, [options])`
> Import multiple files and data into IPFS.
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| source | [FileStream](#filestream) | Data to import (see below) |
##### FileStream
`FileStream` is a stream of [FileContent](#filecontent) or [FileObject](#fileobject) entries of the type:
```js
Iterable | AsyncIterable | ReadableStream
```
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| chunker | `string` | `'size-262144'` | chunking algorithm used to build ipfs DAGs |
| cidVersion | `number` | `0` | the CID version to use when storing the data |
| enableShardingExperiment | `boolean` | `false` | allows to create directories with an unlimited number of entries currently size of unixfs directories is limited by the maximum block size. Note that this is an experimental feature |
| hashAlg | `String` | `'sha2-256'` | multihash hashing algorithm to use |
| onlyHash | `boolean` | `false` | If true, will not add blocks to the blockstore |
| pin | `boolean` | `true` | pin this object when adding |
| progress | function | `undefined` | a function that will be called with the number of bytes added as a file is added to ipfs and the path of the file being added |
| rawLeaves | `boolean` | `false` | if true, DAG leaves will contain raw file data and not be wrapped in a protobuf |
| shardSplitThreshold | `Number` | `1000` | Directories with more than this number of files will be created as HAMT-sharded directories |
| trickle | `boolean` | `false` | if true will use the [trickle DAG](https://godoc.org/github.com/ipsn/go-ipfs/gxlibs/github.com/ipfs/go-unixfs/importer/trickle) format for DAG generation |
| wrapWithDirectory | `boolean` | `false` | Adds a wrapping node around the content |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields objects describing the added data |
Each yielded object is of the form:
```JavaScript
{
path: '/tmp/myfile.txt',
cid: CID('QmHash'),
mode: Number, // implicit if not provided - 0644 for files, 0755 for directories
mtime?: { secs: Number, nsecs: Number },
size: 123
}
```
#### Example
```js
const files = [{
path: '/tmp/myfile.txt',
content: 'ABC'
}]
for await (const result of ipfs.addAll(files)) {
console.log(result)
}
/*
Prints out objects like:
{
"path": "tmp",
"cid": CID("QmWXdjNC362aPDtwHPUE9o2VMqPeNeCQuTBTv1NsKtwypg"),
"mode": 493,
"mtime": { secs: Number, nsecs: Number },
"size": 67
}
{
"path": "/tmp/myfile.txt",
"cid": CID("QmNz1UBzpdd4HfZ3qir3aPiRdX5a93XwTuDNyXRc6PKhWW"),
"mode": 420,
"mtime": { secs: Number, nsecs: Number },
"size": 11
}
*/
```
Now [ipfs.io/ipfs/Qm...WW](https://ipfs.io/ipfs/QmNz1UBzpdd4HfZ3qir3aPiRdX5a93XwTuDNyXRc6PKhWW) returns the "ABC" string.
#### Notes
##### Chunking options
The `chunker` option can be one of the following formats:
- size-{size}
- rabin
- rabin-{avg}
- rabin-{min}-{avg}-{max}
`size-*` will result in fixed-size chunks, `rabin(-*)` will use [rabin fingerprinting](https://en.wikipedia.org/wiki/Rabin_fingerprint) to potentially generate variable size chunks.
##### Hash algorithms
See the [multihash](https://github.com/multiformats/js-multihash/blob/master/src/constants.js#L5-L343) module for the list of all possible values.
##### Importing files from the file system
Both js-ipfs and js-ipfs-http-client export a utility to make importing files from the file system easier (Note: it not available in the browser).
```js
import { create, globSource } from 'ipfs'
const ipfs = await create()
//options specific to globSource
const globSourceOptions = {
recursive: true
};
//example options to pass to IPFS
const addOptions = {
pin: true,
wrapWithDirectory: true,
timeout: 10000
};
for await (const file of ipfs.addAll(globSource('./docs', globSourceOptions), addOptions)) {
console.log(file)
}
/*
{
path: 'docs/assets/anchor.js',
cid: CID('QmVHxRocoWgUChLEvfEyDuuD6qJ4PhdDL2dTLcpUy3dSC2'),
size: 15347
}
{
path: 'docs/assets/bass-addons.css',
hash: CID('QmPiLWKd6yseMWDTgHegb8T7wVS7zWGYgyvfj7dGNt2viQ'),
size: 232
}
...
*/
```
##### Importing a file from a URL
Both js-ipfs and js-ipfs-http-client export a utility to make importing a file from a URL easier.
```js
import { create, urlSource } from 'ipfs'
const ipfs = await create()
const file = await ipfs.add(urlSource('https://ipfs.io/images/ipfs-logo.svg'))
console.log(file)
/*
{
path: 'ipfs-logo.svg',
cid: CID('QmTqZhR6f7jzdhLgPArDPnsbZpvvgxzCZycXK7ywkLxSyU'),
size: 3243
}
*/
```
A great source of [examples](https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/add.js) can be found in the tests for this API.
### `ipfs.cat(ipfsPath, [options])`
> Returns a file addressed by a valid IPFS Path.
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| ipfsPath | String or [CID][] | An [IPFS path][] or CID to export |
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| offset | `Number` | `undefined` | An offset to start reading the file from |
| length | `Number` | `undefined` | An optional max length to read from the file |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields `Uint8Array` objects with the contents of `path` |
#### Example
```JavaScript
for await (const chunk of ipfs.cat(ipfsPath)) {
console.info(chunk)
}
```
A great source of [examples](https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/cat.js) can be found in the tests for this API.
### `ipfs.get(ipfsPath, [options])`
> Fetch a file or an entire directory tree from IPFS that is addressed by a valid IPFS Path.
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| ipfsPath | String or [CID][] | An [IPFS path][] or CID to export |
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| archive | `boolean` | `undefined` | Return the file/directory in a tarball |
| compress | `boolean` | `false` | Gzip the returned stream |
| compressionLevel | `Number` | `undefined` | How much compression to apply (1-9) |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields bytes |
What is streamed as a response depends on the options passed and what the `ipfsPath` resolves to.
1. If `ipfsPath` resolves to a file:
* By default you will get a tarball containing the file
* Pass `compress: true` (and an optional `compressionLevel`) to instead get the gzipped file contents
* Pass `compress: true` (and an optional `compressionLevel`) AND `archive: true` to get a gzipped tarball containing the file
2. If `ipfsPath` resolves to a directory:
* By default you will get a tarball containing the contents of the directory
* Passing `compress: true` will cause an error
* Pass `compress: true` (and an optional `compressionLevel`) AND `archive: true` to get a gzipped tarball containing the contents of the directory
#### Example
```JavaScript
const cid = 'QmQ2r6iMNpky5f1m4cnm3Yqw8VSvjuKpTcK1X7dBR1LkJF'
for await (const buf of ipfs.get(cid)) {
// do something with buf
}
```
A great source of [examples](https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/get.js) can be found in the tests for this API.
### `ipfs.ls(ipfsPath)`
> Lists a directory from IPFS that is addressed by a valid IPFS Path.
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| ipfsPath | String or [CID][] | An [IPFS path][] or CID to list |
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields objects representing the files |
Each yielded object is of the form:
```js
{
depth: 1,
name: 'alice.txt',
path: 'QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/alice.txt',
size: 11696,
cid: CID('QmZyUEQVuRK3XV7L9Dk26pg6RVSgaYkiSTEdnT2kZZdwoi'),
type: 'file',
mode: Number, // implicit if not provided - 0644 for files, 0755 for directories
mtime?: { secs: Number, nsecs: Number }
}
```
#### Example
```JavaScript
const cid = 'QmQ2r6iMNpky5f1m4cnm3Yqw8VSvjuKpTcK1X7dBR1LkJF'
for await (const file of ipfs.ls(cid)) {
console.log(file.path)
}
```
A great source of [examples](https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/ls.js) can be found in the tests for this API.
---
## The Mutable Files API
The Mutable File System (MFS) is a virtual file system on top of IPFS that exposes a Unix like API over a virtual directory. It enables users to write and read from paths without having to worry about updating the graph. It enables things like [ipfs-blob-store](https://github.com/ipfs/ipfs-blob-store) to exist.
### `ipfs.files.chmod(path, mode, [options])`
> Change mode for files and directories
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| path | String or [CID][] | An [MFS Path][], [IPFS path][] or CID to modify |
| mode | String or Number | An integer (e.g. `0o755` or `parseInt('0755', 8)`) or a string modification of the existing mode, e.g. `'a+x'`, `'g-w'`, etc |
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| recursive | `boolean` | `false` | If true `mode` will be applied to the entire tree under `path` |
| flush | `boolean` | `true` | If true the changes will be immediately flushed to disk |
| hashAlg | `String` | `'sha2-256'` | The hash algorithm to use for any updated entries |
| cidVersion | `Number` | `0` | The CID version to use for any updated entries |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
#### Example
```JavaScript
// To give a file -rwxrwxrwx permissions
await ipfs.files.chmod('/path/to/file.txt', parseInt('0777', 8))
// Alternatively
await ipfs.files.chmod('/path/to/file.txt', '+rwx')
// You can omit the leading `0` too
await ipfs.files.chmod('/path/to/file.txt', '777')
```
### `ipfs.files.cp(...from, to, [options])`
> Copy files from one location to another
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| from | One or more Strings or [CID][]s | An [MFS path][], [IPFS path][] or CID |
| to | `String` | An [MFS path][] |
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| parents | `boolean` | `false` | If true, create intermediate directories |
| flush | `boolean` | `true` | If true the changes will be immediately flushed to disk |
| hashAlg | `String` | `'sha2-256'` | The hash algorithm to use for any updated entries |
| cidVersion | `Number` | `0` | The CID version to use for any updated entries |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
#### Example
```JavaScript
// To copy a file
await ipfs.files.cp('/src-file', '/dst-file')
// To copy a directory
await ipfs.files.cp('/src-dir', '/dst-dir')
// To copy multiple files to a directory
await ipfs.files.cp(['/src-file1', '/src-file2'], '/dst-dir')
```
#### Notes
If `from` has multiple values then `to` must be a directory.
If `from` has a single value and `to` exists and is a directory, `from` will be copied into `to`.
If `from` has a single value and `to` exists and is a file, `from` must be a file and the contents of `to` will be replaced with the contents of `from` otherwise an error will be returned.
If `from` is an IPFS path, and an MFS path exists with the same name, the IPFS path will be chosen.
If `from` is an IPFS path and the content does not exist in your node's repo, only the root node of the source file with be retrieved from the network and linked to from the destination. The remainder of the file will be retrieved on demand.
### `ipfs.files.mkdir(path, [options])`
> Make a directory in your MFS
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| path | `String` | The [MFS path][] to create a directory at |
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| parents | `boolean` | `false` | If true, create intermediate directories |
| mode | `Number` | `undefined` | An integer that represents the file mode |
| mtime | `Object` | `undefined` | A Date object, an object with `{ secs, nsecs }` properties where `secs` is the number of seconds since (positive) or before (negative) the Unix Epoch began and `nsecs` is the number of nanoseconds since the last full second, or the output of [`process.hrtime()`](https://nodejs.org/api/process.html#process_process_hrtime_time) |
| flush | `boolean` | `true` | If true the changes will be immediately flushed to disk |
| hashAlg | `String` | `'sha2-256'` | The hash algorithm to use for any updated entries |
| cidVersion | `Number` | `0` | The CID version to use for any updated entries |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
#### Example
```JavaScript
await ipfs.files.mkdir('/my/beautiful/directory')
```
### `ipfs.files.stat(path, [options])`
> Get file or directory statistics
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| path | `String` | The [MFS path][] return statistics from |
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| hash | `boolean` | `false` | If true, return only the CID |
| size | `boolean` | `false` | If true, return only the size |
| withLocal | `boolean` | `false` | If true, compute the amount of the DAG that is local and if possible the total size |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object containing the file/directory status |
the returned object has the following keys:
- `cid` a [CID][cid] instance
- `size` is an integer with the file size in Bytes
- `cumulativeSize` is an integer with the size of the DAGNodes making up the file in Bytes
- `type` is a string that can be either `directory` or `file`
- `blocks` if `type` is `directory`, this is the number of files in the directory. If it is `file` it is the number of blocks that make up the file
- `withLocality` is a boolean to indicate if locality information is present
- `local` is a boolean to indicate if the queried dag is fully present locally
- `sizeLocal` is an integer indicating the cumulative size of the data present locally
#### Example
```JavaScript
const stats = await ipfs.files.stat('/file.txt')
console.log(stats)
// {
// hash: CID('QmXmJBmnYqXVuicUfn9uDCC8kxCEEzQpsAbeq1iJvLAmVs'),
// size: 60,
// cumulativeSize: 118,
// blocks: 1,
// type: 'file'
// }
```
### `ipfs.files.touch(path, [options])`
> Update the mtime of a file or directory
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| path | `String` | The [MFS path][] to update the mtime for |
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| mtime | `Object` | Now | Either a ` Date` object, an object with `{ sec, nsecs }` properties or the output of `process.hrtime()` |
| flush | `boolean` | `true` | If true the changes will be immediately flushed to disk |
| hashAlg | `String` | `'sha2-256'` | The hash algorithm to use for any updated entries |
| cidVersion | `Number` | `0` | The CID version to use for any updated entries |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
#### Example
```JavaScript
// set the mtime to the current time
await ipfs.files.touch('/path/to/file.txt')
// set the mtime to a specific time
await ipfs.files.touch('/path/to/file.txt', {
mtime: new Date('May 23, 2014 14:45:14 -0700')
})
```
### `ipfs.files.rm(path, [options])`
> Remove a file or directory.
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| path | `String` or `Array` | One or more [MFS path][]s to remove |
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| recursive | `boolean` | `false` | If true all paths under the specifed path(s) will be removed |
| flush | `boolean` | `true` | If true the changes will be immediately flushed to disk |
| hashAlg | `String` | `'sha2-256'` | The hash algorithm to use for any updated entries |
| cidVersion | `Number` | `0` | The CID version to use for any updated entries |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
#### Example
```JavaScript
// To remove a file
await ipfs.files.rm('/my/beautiful/file.txt')
// To remove multiple files
await ipfs.files.rm(['/my/beautiful/file.txt', '/my/other/file.txt'])
// To remove a directory
await ipfs.files.rm('/my/beautiful/directory', { recursive: true })
```
### `ipfs.files.read(path, [options])`
> Read a file
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| path | `String` or [CID][] | An [MFS path][], [IPFS Path][] or [CID][] to read |
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| offset | `Number` | `undefined` | An offset to start reading the file from |
| length | `Number` | `undefined` | An optional max length to read from the file |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields `Uint8Array` objects with the contents of `path` |
#### Example
```JavaScript
const chunks = []
for await (const chunk of ipfs.files.read('/hello-world')) {
chunks.push(chunk)
}
console.log(uint8ArrayConcat(chunks).toString())
// Hello, World!
```
### `ipfs.files.write(path, content, [options])`
> Write to an MFS path
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| path | `String` | The [MFS path] where you will write to |
| content | `String`, `Uint8Array`, `AsyncIterable` or [`Blob`][blob] | The content to write to the path |
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| offset | `Number` | `undefined` | An offset to start writing to file at |
| length | `Number` | `undefined` | Optionally limit how many bytes are read from the stream |
| create | `boolean` | `false` | Create the MFS path if it does not exist |
| parents | `boolean` | `false` | Create intermediate MFS paths if they do not exist |
| truncate | `boolean` | `false` | Truncate the file at the MFS path if it would have been larger than the passed `content` |
| rawLeaves | `boolean` | `false ` | If true, DAG leaves will contain raw file data and not be wrapped in a protobuf |
| mode | `Number` | `undefined` | An integer that represents the file mode |
| mtime | `Object` | `undefined` | A Date object, an object with `{ secs, nsecs }` properties where `secs` is the number of seconds since (positive) or before (negative) the Unix Epoch began and `nsecs` is the number of nanoseconds since the last full second, or the output of [`process.hrtime()`](https://nodejs.org/api/process.html#process_process_hrtime_time) |
| flush | `boolean` | `true` | If true the changes will be immediately flushed to disk |
| hashAlg | `String` | `'sha2-256'` | The hash algorithm to use for any updated entries |
| cidVersion | `Number` | `0` | The CID version to use for any updated entries |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
#### Example
```JavaScript
await ipfs.files.write('/hello-world', new TextEncoder().encode('Hello, world!'))
```
### `ipfs.files.mv(...from, to, [options])`
> Move files from one location to another
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| ...from | `String` | One or more [MFS path][]s to move |
| to | `String` | The location to move files to |
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| parents | `boolean` | `false` | Create intermediate MFS paths if they do not exist |
| flush | `boolean` | `true` | If true the changes will be immediately flushed to disk |
| hashAlg | `String` | `'sha2-256'` | The hash algorithm to use for any updated entries |
| cidVersion | `Number` | `0` | The CID version to use for any updated entries |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
#### Example
```JavaScript
await ipfs.files.mv('/src-file', '/dst-file')
await ipfs.files.mv('/src-dir', '/dst-dir')
await ipfs.files.mv(['/src-file1', '/src-file2'], '/dst-dir')
```
#### Notes
If `from` has multiple values then `to` must be a directory.
If `from` has a single value and `to` exists and is a directory, `from` will be moved into `to`.
If `from` has a single value and `to` exists and is a file, `from` must be a file and the contents of `to` will be replaced with the contents of `from` otherwise an error will be returned.
If `from` is an IPFS path, and an MFS path exists with the same name, the IPFS path will be chosen.
If `from` is an IPFS path and the content does not exist in your node's repo, only the root node of the source file with be retrieved from the network and linked to from the destination. The remainder of the file will be retrieved on demand.
All values of `from` will be removed after the operation is complete unless they are an IPFS path.
### `ipfs.files.flush(path, [options])`
> Flush a given path's data to the disk
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| path | `String` | The [MFS path][] to flush |
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | The CID of the path that has been flushed |
#### Example
```JavaScript
const cid = await ipfs.files.flush('/')
```
### `ipfs.files.ls(path, [options])`
> List directories in the local mutable namespace
#### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| path | `String` | The [MFS path][] to list |
#### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
#### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields objects representing the files |
Each object contains the following keys:
- `name` which is the file's name
- `type` which is the object's type (`directory` or `file`)
- `size` the size of the file in bytes
- `cid` the hash of the file (A [CID][cid] instance)
- `mode` the UnixFS mode as a Number
- `mtime` an objects with numeric `secs` and `nsecs` properties
#### Example
```JavaScript
for await (const file of ipfs.files.ls('/screenshots')) {
console.log(file.name)
}
// 2018-01-22T18:08:46.775Z.png
// 2018-01-22T18:08:49.184Z.png
```
[b]: https://www.npmjs.com/package/buffer
[file]: https://developer.mozilla.org/en-US/docs/Web/API/File
[cid]: https://docs.ipfs.io/concepts/content-addressing
[blob]: https://developer.mozilla.org/en-US/docs/Web/API/Blob
[IPFS Path]: https://www.npmjs.com/package/is-ipfs#isipfspathpath
[MFS Path]: https://docs.ipfs.io/guides/concepts/mfs/
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
================================================
FILE: docs/core-api/KEY.md
================================================
# Key API
- [`ipfs.key.gen(name, [options])`](#ipfskeygenname-options)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.key.list([options])`](#ipfskeylistoptions)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [`ipfs.key.rm(name, [options])`](#ipfskeyrmname-options)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-2)
- [Example](#example-2)
- [`ipfs.key.rename(oldName, newName, [options])`](#ipfskeyrenameoldname-newname-options)
- [Parameters](#parameters-3)
- [Options](#options-3)
- [Returns](#returns-3)
- [Example](#example-3)
- [`ipfs.key.export(name, password, [options])`](#ipfskeyexportname-password-options)
- [Parameters](#parameters-4)
- [Options](#options-4)
- [Returns](#returns-4)
- [Example](#example-4)
- [`ipfs.key.import(name, pem, password, [options])`](#ipfskeyimportname-pem-password-options)
- [Parameters](#parameters-5)
- [Options](#options-5)
- [Returns](#returns-5)
- [Example](#example-5)
## `ipfs.key.gen(name, [options])`
> Generate a new key
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| name | String | The name to give the key |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| type | `String` | `'rsa'` | The key type, one of `'rsa'` or `'ed25519'` |
| size | `Number` | `2048` | The key size in bits |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object that describes the key; `name` and `id` |
### Example
```JavaScript
const key = await ipfs.key.gen('my-key', {
type: 'rsa',
size: 2048
})
console.log(key)
// { id: 'QmYWqAFvLWb2G5A69JGXui2JJXzaHXiUEmQkQgor6kNNcJ',
// name: 'my-key' }
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.key.list([options])`
> List all the keys
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An array representing all the keys |
example of the returned array:
```js
{
id: 'hash', // string - the hash of the key
name: 'self' // string - the name of the key
}
```
### Example
```JavaScript
const keys = await ipfs.key.list()
console.log(keys)
// [
// { id: 'QmTe4tuceM2sAmuZiFsJ9tmAopA8au71NabBDdpPYDjxAb',
// name: 'self' },
// { id: 'QmWETF5QvzGnP7jKq5sPDiRjSM2fzwzNsna4wSBEzRzK6W',
// name: 'my-key' }
// ]
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.key.rm(name, [options])`
> Remove a key
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| name | String | The name of the key to remove |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object that describes the removed key |
example of the returned object:
```js
{
id: 'hash', // string - the hash of the key
name: 'self' // string - the name of the key
}
```
### Example
```JavaScript
const key = await ipfs.key.rm('my-key')
console.log(key)
// { id: 'QmWETF5QvzGnP7jKq5sPDiRjSM2fzwzNsna4wSBEzRzK6W',
// name: 'my-key' }
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.key.rename(oldName, newName, [options])`
> Rename a key
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| oldName | String | The current key name |
| newName | String | The desired key name |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object that describes the renamed key |
### Example
```JavaScript
const key = await ipfs.key.rename('my-key', 'my-new-key')
console.log(key)
// { id: 'Qmd4xC46Um6s24MradViGLFtMitvrR4SVexKUgPgFjMNzg',
// was: 'my-key',
// now: 'my-new-key',
// overwrite: false }
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.key.export(name, password, [options])`
> Export a key in a PEM encoded password protected PKCS #8
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| name | String | The name of the key to export |
| password | String | Password to set on the PEM output |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | The string representation of the key |
### Example
```JavaScript
const pem = await ipfs.key.export('self', 'password')
console.log(pem)
// -----BEGIN ENCRYPTED PRIVATE KEY-----
// MIIFDTA/BgkqhkiG9w0BBQ0wMjAaBgkqhkiG9w0BBQwwDQQIpdO40RVyBwACAWQw
// ...
// YA==
// -----END ENCRYPTED PRIVATE KEY-----
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.key.import(name, pem, password, [options])`
> Import a PEM encoded password protected PKCS #8 key
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| name | String | The name of the key to export |
| pem | String | The PEM encoded key |
| password | String | The password that protects the PEM key |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object that describes the new key |
### Example
```JavaScript
const key = await ipfs.key.import('clone', pem, 'password')
console.log(key)
// { id: 'QmQRiays958UM7norGRQUG3tmrLq8pJdmJarwYSk2eLthQ',
// name: 'clone' }
```
A great source of [examples][] can be found in the tests for this API.
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/key
[cid]: https://docs.ipfs.io/concepts/content-addressing
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
================================================
FILE: docs/core-api/MISCELLANEOUS.md
================================================
# Miscellaneous API
- [`ipfs.id([options])`](#ipfsidoptions)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.version([options])`](#ipfsversionoptions)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [`ipfs.dns(domain, [options])`](#ipfsdnsdomain-options)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-2)
- [Example](#example-2)
- [`ipfs.stop([options])`](#ipfsstopoptions)
- [Parameters](#parameters-3)
- [Options](#options-3)
- [Returns](#returns-3)
- [Example](#example-3)
- [`ipfs.ping(peerId, [options])`](#ipfspingpeerid-options)
- [Parameters](#parameters-4)
- [Options](#options-4)
- [Returns](#returns-4)
- [Example](#example-4)
- [`ipfs.resolve(name, [options])`](#ipfsresolvename-options)
- [Parameters](#parameters-5)
- [Options](#options-5)
- [Returns](#returns-5)
- [Example](#example-5)
## `ipfs.id([options])`
> Returns the identity of the Peer
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
| peerId | `string` | `undefined` | Look up the identity for this peer instead of the current node |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object with the Peer identity |
The Peer identity has the following properties:
- `id: String` - the Peer ID
- `publicKey: String` - the public key of the peer as a base64 encoded string
- `addresses: Multiaddr[]` - A list of multiaddrs this node is listening on
- `agentVersion: String` - The agent version
- `protocolVersion: String` - The supported protocol version
- `protocols: String[]` - The supported protocols
### Example
```JavaScript
const identity = await ipfs.id()
console.log(identity)
```
A great source of [examples](https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/miscellaneous/id.js) can be found in the tests for this API.
## `ipfs.version([options])`
> Returns the implementation version
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object with the version of the implementation, the commit and the Repo. `js-ipfs` instances will also return the version of `interface-ipfs-core` and `ipfs-http-client` supported by this node |
### Example
```JavaScript
const version = await ipfs.version()
console.log(version)
```
A great source of [examples](https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/miscellaneous/version.js) can be found in the tests for this API.
## `ipfs.dns(domain, [options])`
> Resolve DNS links
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| domain | String | The domain to resolve |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| recursive | `boolean` | `true` | Resolve until result is not a domain name |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | A string representing the IPFS path for that domain |
### Example
```JavaScript
const path = await ipfs.dns('ipfs.io')
console.log(path)
```
A great source of [examples](https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/miscellaneous/dns.js) can be found in the tests for this API.
## `ipfs.stop([options])`
> Stops the IPFS node and in case of talking with an IPFS Daemon, it stops the process.
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
### Example
```JavaScript
await ipfs.stop()
```
A great source of [examples](https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/miscellaneous/stop.js) can be found in the tests for this API.
## `ipfs.ping(peerId, [options])`
> Send echo request packets to IPFS hosts
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| peerId | [PeerID][] or [CID][] | The remote peer to send packets to |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| count | `Number` | `10` | The number of ping messages to send |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
Where:
- `peerId` (string) ID of the peer to be pinged.
- `options` is an optional object argument that might include the following properties:
- `count` (integer, default 10): the number of ping messages to send
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields ping response objects |
Each yielded object is of the form:
```js
{
success: true,
time: 1234,
text: ''
}
```
Note that not all ping response objects are "pongs". A "pong" message can be identified by a truthy `success` property and an empty `text` property. Other ping responses are failures or status updates.
### Example
```JavaScript
for await (const res of ipfs.ping('Qmhash')) {
if (res.time) {
console.log(`Pong received: time=${res.time} ms`)
} else {
console.log(res.text)
}
}
```
A great source of [examples](https://github.com/ipfs/js-ipfs/tree/master/packages/interface-ipfs-core/src/ping) can be found in the tests for this API.
## `ipfs.resolve(name, [options])`
> Resolve the value of names to IPFS
There are a number of mutable name protocols that can link among themselves and into IPNS. For example IPNS references can (currently) point at an IPFS object, and DNS links can point at other DNS links, IPNS entries, or IPFS objects. This command accepts any of these identifiers and resolves them to the referenced item.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| name | String | The name to resolve |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| recursive | `boolean` | `true` | Resolve until result is an IPFS name |
| cidBase | `String` | `base58btc` | Multibase codec name the CID in the resolved path will be encoded with |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | A string representing the resolved name |
### Example
Resolve the value of your identity:
```JavaScript
const name = '/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy'
const res = await ipfs.resolve(name)
console.log(res) // /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj
```
Resolve the value of another name recursively:
```JavaScript
const name = '/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n'
// Where:
// /ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n
// ...resolves to:
// /ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
// ...which in turn resolves to:
// /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj
const res = await ipfs.resolve(name, { recursive: true })
console.log(res) // /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj
```
Resolve the value of an IPFS path:
```JavaScript
const name = '/ipfs/QmeZy1fGbwgVSrqbfh9fKQrAWgeyRnj7h8fsHS1oy3k99x/beep/boop'
const res = await ipfs.resolve(name)
console.log(res) // /ipfs/QmYRMjyvAiHKN9UTi8Bzt1HUspmSRD8T8DwxfSMzLgBon1
```
A great source of [examples](https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/miscellaneous/resolve.js) can be found in the tests for this API.
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/miscellaneous
[rs]: https://www.npmjs.com/package/readable-stream
[ps]: https://www.npmjs.com/package/pull-stream
[cid]: https://docs.ipfs.io/concepts/content-addressing
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
================================================
FILE: docs/core-api/NAME.md
================================================
# Name API
- [`ipfs.name.publish(value, [options])`](#ipfsnamepublishvalue-options)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [Notes](#notes)
- [`ipfs.name.pubsub.cancel(name, [options])`](#ipfsnamepubsubcancelname-options)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [`ipfs.name.pubsub.state([options])`](#ipfsnamepubsubstateoptions)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-2)
- [Example](#example-2)
- [`ipfs.name.pubsub.subs([options])`](#ipfsnamepubsubsubsoptions)
- [Parameters](#parameters-3)
- [Options](#options-3)
- [Returns](#returns-3)
- [Example](#example-3)
- [`ipfs.name.resolve(value, [options])`](#ipfsnameresolvevalue-options)
- [Parameters](#parameters-4)
- [Options](#options-4)
- [Returns](#returns-4)
- [Example](#example-4)
## `ipfs.name.publish(value, [options])`
> Publish an IPNS name with a given value.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| value | [CID][] | The content to publish |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| resolve | `boolean` | `true` | Resolve given path before publishing |
| lifetime | `String` | `24h` | Time duration of the record |
| ttl | `String` | `undefined` | Time duration this record should be cached |
| key | `String` | `'self'` | Name of the key to be used |
| allowOffline | `boolean` | `true` | When offline, save the IPNS record to the the local datastore without broadcasting to the network instead of simply failing. |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object that contains the IPNS hash and the IPFS hash |
example of the returned object:
```JavaScript
{
name: "/ipns/QmHash.."
value: "/ipfs/QmHash.."
}
```
### Example
Imagine you want to publish your website under IPFS. You can use the [Files API](./FILES.md) to publish your static website and then you'll get a multihash you can link to. But when you need to make a change, a problem arises: you get a new multihash because you now have a different content. And it is not possible for you to be always giving others the new address.
Here's where the Name API comes in handy. With it, you can use one static multihash for your website under IPNS (InterPlanetary Name Service). This way, you can have one single multihash poiting to the newest version of your website.
```JavaScript
// The address of your files.
const addr = '/ipfs/QmbezGequPwcsWo8UL4wDF6a8hYwM1hmbzYv2mnKkEWaUp'
const res = await ipfs.name.publish(addr)
// You now have a res which contains two fields:
// - name: the name under which the content was published.
// - value: the "real" address to which Name points.
console.log(`https://gateway.ipfs.io/ipns/${res.name}`)
```
This way, you can republish a new version of your website under the same address. By default, `ipfs.name.publish` will use the Peer ID. If you want to have multiple websites (for example) under the same IPFS module, you can always check the [key API](./KEY.md).
A great source of [examples][] can be found in the tests for this API.
### Notes
The `allowOffline` option is not yet implemented in js-ipfs. See tracking issue [ipfs/js-ipfs#1997](https://github.com/ipfs/js-ipfs/issues/1997).
## `ipfs.name.pubsub.cancel(name, [options])`
> Cancel a name subscription
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| name | `String` | The name of the subscription to cancel |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
`arg` is the name of the subscription to cancel.
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object that contains the result of the operation |
example of the returned object:
```JavaScript
{
canceled: true
}
```
### Example
```JavaScript
const name = 'QmQrX8hka2BtNHa8N8arAq16TCVx5qHcb46c5yPewRycLm'
const result = await ipfs.name.pubsub.cancel(name)
console.log(result.canceled)
// true
```
A great source of [examples][examples-pubsub] can be found in the tests for this API.
## `ipfs.name.pubsub.state([options])`
> Query the state of IPNS pubsub
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object that contains the result of the operation |
example of the returned object:
```JavaScript
{
enabled: true
}
```
### Example
```JavaScript
const result = await ipfs.name.pubsub.state()
console.log(result.enabled)
// true
```
A great source of [examples][examples-pubsub] can be found in the tests for this API.
## `ipfs.name.pubsub.subs([options])`
> Show current name subscriptions
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An array of subscriptions |
example of the returned array:
```JavaScript
['/ipns/QmQrX8hka2BtNHa8N8arAq16TCVx5qHcb46c5yPewRycLm']
```
### Example
```JavaScript
const result = await ipfs.name.pubsub.subs()
console.log(result)
// ['/ipns/QmQrX8hka2BtNHa8N8arAq16TCVx5qHcb46c5yPewRycLm']
```
A great source of [examples][examples-pubsub] can be found in the tests for this API.
## `ipfs.name.resolve(value, [options])`
> Resolve an IPNS name.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| value | `PeerId` or `string` | An IPNS address such as `/ipns/ipfs.io` |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| recursive | `boolean` | `false` | Resolve until the result is not an IPNS name |
| nocache | `boolean` | `cache` | Do not use cached entries |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields strings that are increasingly more accurate resolved paths. |
### Example
```JavaScript
// The IPNS address you want to resolve.
const addr = '/ipns/ipfs.io'
for await (const name of ipfs.name.resolve(addr)) {
console.log(name)
// /ipfs/QmQrX8hka2BtNHa8N8arAq16TCVx5qHcb46c5yPewRycLm
}
```
A great source of [examples][] can be found in the tests for this API.
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/name
[examples-pubsub]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/name-pubsub
[cid]: https://docs.ipfs.io/concepts/content-addressing
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
================================================
FILE: docs/core-api/OBJECT.md
================================================
# Object API
> ⚠️ Object API is [deprecated](https://github.com/ipfs/go-ipfs/issues/7936), use [FILES](FILES.md) and [DAG](DAG.md) APIs instead.
- [`ipfs.object.new([options])`](#ipfsobjectnewoptions)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.object.put(obj, [options])`](#ipfsobjectputobj-options)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [`ipfs.object.get(cid, [options])`](#ipfsobjectgetcid-options)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-2)
- [Example](#example-2)
- [`ipfs.object.data(cid, [options])`](#ipfsobjectdatacid-options)
- [Parameters](#parameters-3)
- [Options](#options-3)
- [Returns](#returns-3)
- [Example](#example-3)
- [`ipfs.object.links(cid, [options])`](#ipfsobjectlinkscid-options)
- [Parameters](#parameters-4)
- [Options](#options-4)
- [Returns](#returns-4)
- [Example](#example-4)
- [`ipfs.object.stat(cid, [options])`](#ipfsobjectstatcid-options)
- [Parameters](#parameters-5)
- [Options](#options-5)
- [Returns](#returns-5)
- [Example](#example-5)
- [`ipfs.object.patch.addLink(cid, link, [options])`](#ipfsobjectpatchaddlinkcid-link-options)
- [Parameters](#parameters-6)
- [Options](#options-6)
- [Returns](#returns-6)
- [Example](#example-6)
- [Notes](#notes)
- [`ipfs.object.patch.rmLink(cid, link, [options])`](#ipfsobjectpatchrmlinkcid-link-options)
- [Parameters](#parameters-7)
- [Options](#options-7)
- [Returns](#returns-7)
- [Example](#example-7)
- [Notes](#notes-1)
- [`ipfs.object.patch.appendData(cid, data, [options])`](#ipfsobjectpatchappenddatacid-data-options)
- [Parameters](#parameters-8)
- [Options](#options-8)
- [Returns](#returns-8)
- [Example](#example-8)
- [`ipfs.object.patch.setData(multihash, data, [options])`](#ipfsobjectpatchsetdatamultihash-data-options)
- [Parameters](#parameters-9)
- [Options](#options-9)
- [Returns](#returns-9)
- [Example](#example-9)
## `ipfs.object.new([options])`
> Create a new MerkleDAG node, using a specific layout. Caveat: So far, only UnixFS object layouts are supported.
### Parameters
None.
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| template | `String` | If defined, must be a string `unixfs-dir` and if that is passed, the created node will be an empty unixfs style directory |
| recursive | `boolean` | `false` | Resolve until the result is not an IPNS name |
| nocache | `boolean` | `cache` | Do not use cached entries |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | A [CID](https://github.com/ipfs/js-cid) instance |
### Example
```JavaScript
const cid = await ipfs.object.new({
template: 'unixfs-dir'
})
console.log(cid.toString())
// Logs:
// QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.object.put(obj, [options])`
> Store a MerkleDAG node.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| obj | `Object{ Data: , Links: [] }`, `Uint8Array` or [DAGNode][] | The MerkleDAG Node to be stored |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| enc | `String` | `undefined` | The encoding of the Uint8Array (json, yml, etc), if passed a Uint8Array |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | A [CID](https://github.com/ipfs/js-cid) instance |
### Example
```JavaScript
const obj = {
Data: new TextEncoder().encode('Some data'),
Links: []
}
const cid = await ipfs.object.put(obj)
console.log(cid.toString())
// Logs:
// QmPb5f92FxKPYdT3QNBd1GKiL4tZUXUrzF4Hkpdr3Gf1gK
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.object.get(cid, [options])`
> Fetch a MerkleDAG node
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][] | The returned [DAGNode][] will correspond to this CID |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | A MerkleDAG node of the type [DAGNode][] |
### Example
```JavaScript
const multihash = 'QmPb5f92FxKPYdT3QNBd1GKiL4tZUXUrzF4Hkpdr3Gf1gK'
const node = await ipfs.object.get(multihash)
console.log(node.Data)
// Logs:
// some data
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.object.data(cid, [options])`
> Returns the Data field of an object
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][] | The returned data will be from the [DAGNode][] that corresponds to this CID |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An Promise that resolves to Uint8Array objects with the data that the MerkleDAG node contained |
### Example
```JavaScript
const cid = 'QmPb5f92FxKPYdT3QNBd1GKiL4tZUXUrzF4Hkpdr3Gf1gK'
const data = await ipfs.object.data(cid)
console.log(data.toString())
// Logs:
// some data
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.object.links(cid, [options])`
> Returns the Links field of an object
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][] | The returned [DAGLink][]s will be from the [DAGNode][] that corresponds to this CID |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An Array of [DAGLink](https://github.com/ipld/js-ipld-dag-pb/blob/master/src/dag-link/dagLink.js) objects |
### Example
```JavaScript
const multihash = 'Qmc5XkteJdb337s7VwFBAGtiaoj2QCEzyxtNRy3iMudc3E'
const links = await ipfs.object.links(multihash)
const hashes = links.map((link) => link.Hash.toString())
console.log(hashes)
// Logs:
// [
// 'QmZbj5ruYneZb8FuR9wnLqJCpCXMQudhSdWhdhp5U1oPWJ',
// 'QmSo73bmN47gBxMNqbdV6rZ4KJiqaArqJ1nu5TvFhqqj1R'
// ]
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.object.stat(cid, [options])`
> Returns stats about an Object
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][] | The returned stats will be from the [DAGNode][] that corresponds to this CID |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object representing the stats of the Object |
the returned object has the following format:
```JavaScript
{
Hash: 'QmPTkMuuL6PD8L2SwTwbcs1NPg14U8mRzerB1ZrrBrkSDD',
NumLinks: 0,
BlockSize: 10,
LinksSize: 2,
DataSize: 8,
CumulativeSize: 10
}
```
### Example
```JavaScript
const multihash = 'QmPTkMuuL6PD8L2SwTwbcs1NPg14U8mRzerB1ZrrBrkSDD'
const stats = await ipfs.object.stat(multihash, {timeout: '10s'})
console.log(stats)
// Logs:
// {
// Hash: 'QmPTkMuuL6PD8L2SwTwbcs1NPg14U8mRzerB1ZrrBrkSDD',
// NumLinks: 0,
// BlockSize: 10,
// LinksSize: 2,
// DataSize: 8,
// CumulativeSize: 10
// }
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.object.patch.addLink(cid, link, [options])`
> Add a Link to an existing MerkleDAG Object
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][] | Add a link to the [DAGNode][] that corresponds to this CID |
| link | [DAGLink][] | The link to add |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An instance of [CID][] representing the new DAG node that was created due to the operation |
### Example
```JavaScript
// cid is CID of the DAG node created by adding a link
const cid = await ipfs.object.patch.addLink(node, {
name: 'some-link',
size: 10,
cid: CID.parse('QmPTkMuuL6PD8L2SwTwbcs1NPg14U8mRzerB1ZrrBrkSDD')
})
```
A great source of [examples][] can be found in the tests for this API.
### Notes
The `DAGLink` to be added can also be passed as an object containing: `name`, `cid` and `size` properties:
```js
const link = {
name: 'Qmef7ScwzJUCg1zUSrCmPAz45m8uP5jU7SLgt2EffjBmbL',
size: 37,
cid: CID.parse('Qmef7ScwzJUCg1zUSrCmPAz45m8uP5jU7SLgt2EffjBmbL')
};
```
or
```js
const link = new DAGLink(name, size, multihash)
```
## `ipfs.object.patch.rmLink(cid, link, [options])`
> Remove a Link from an existing MerkleDAG Object
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][] | Remove a link to the [DAGNode][] that corresponds to this CID |
| link | [DAGLink][] | The [DAGLink][] to remove |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An instance of [CID][] representing the new DAG node that was created due to the operation |
### Example
```JavaScript
// cid is CID of the DAG node created by removing a link
const cid = await ipfs.object.patch.rmLink(node, {
Name: 'some-link',
Tsize: 10,
Hash: CID.parse('QmPTkMuuL6PD8L2SwTwbcs1NPg14U8mRzerB1ZrrBrkSDD')
})
```
A great source of [examples][] can be found in the tests for this API.
### Notes
`link` is the link to be removed on the node that is identified by the `multihash`, can be passed as:
- `DAGLink`
```js
const link = new DAGLink(name, size, multihash)
```
- Object containing a `name` property
```js
const link = {
name: 'Qmef7ScwzJUCg1zUSrCmPAz45m8uP5jU7SLgt2EffjBmbL'
};
```
## `ipfs.object.patch.appendData(cid, data, [options])`
> Append Data to the Data field of an existing node
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][] | Add data to the [DAGNode][] that corresponds to this CID |
| data | `Uint8Array` | The data to append to the `.Data` field of the node |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An instance of [CID][] representing the new DAG node that was created due to the operation |
### Example
```JavaScript
const cid = await ipfs.object.patch.appendData(multihash, new TextEncoder().encode('more data'))
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.object.patch.setData(multihash, data, [options])`
> Overwrite the Data field of a DAGNode with new Data
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][] | Replace data of the [DAGNode][] that corresponds to this CID |
| data | `Uint8Array` | The data to overwrite with |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An instance of [CID][] representing the new DAG node that was created due to the operation |
### Example
```JavaScript
const cid = '/ipfs/Qmfoo'
const updatedCid = await ipfs.object.patch.setData(cid, new TextEncoder().encode('more data'))
```
A great source of [examples][] can be found in the tests for this API.
[CID]: https://github.com/multiformats/js-cid
[DAGNode]: https://github.com/ipld/js-ipld-dag-pb
[DAGLink]: https://github.com/ipld/js-ipld-dag-pb
[multihash]: http://github.com/multiformats/multihash
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/object
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
================================================
FILE: docs/core-api/PIN.md
================================================
# Pin API
- [`ipfs.pin.add(ipfsPath, [options])`](#ipfspinaddipfspath-options)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.pin.addAll(source, [options])`](#ipfspinaddallsource-options)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [`ipfs.pin.ls([options])`](#ipfspinlsoptions)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-2)
- [Example](#example-2)
- [`ipfs.pin.rm(ipfsPath, [options])`](#ipfspinrmipfspath-options)
- [Parameters](#parameters-3)
- [Options](#options-3)
- [Returns](#returns-3)
- [Example](#example-3)
- [`ipfs.pin.rmAll(source, [options])`](#ipfspinrmallsource-options)
- [Parameters](#parameters-4)
- [Options](#options-4)
- [Returns](#returns-4)
- [Example](#example-4)
- [`ipfs.pin.remote.service.add(name, options)`](#ipfspinremoteserviceaddname-options)
- [Parameters](#parameters-5)
- [Options](#options-5)
- [Returns](#returns-5)
- [Example](#example-5)
- [`ipfs.pin.remote.service.ls([options])`](#ipfspinremoteservicels_options)
- [Options](#options-6)
- [Returns](#returns-6)
- [Example](#example-6)
- [`ipfs.pin.remote.service.rm(name, [options])`](#ipfspinremoteservicermname-options)
- [Parameters](#parameters-6)
- [Options](#options-7)
- [Returns](#returns-7)
- [Example](#example-7)
- [`ipfs.pin.remote.add(cid, [options])`](#ipfspinremoteaddcid-options)
- [Parameters](#parameters-7)
- [Options](#options-8)
- [Returns](#returns-8)
- [Example](#example-8)
- [`ipfs.pin.remote.ls(options)`](#ipfspinremotelsoptions)
- [Options](#options-9)
- [Returns](#returns-9)
- [Example](#example-9)
- [`ipfs.pin.remote.rm(options)`](#ipfspinremotermoptions)
- [Options](#options-10)
- [Returns](#returns-10)
- [Example](#example-10)
- [`ipfs.pin.remote.rmAll(options)`](#ipfspinremotermalloptions)
- [Options](#options-11)
- [Returns](#returns-11)
- [Example](#example-11)
## `ipfs.pin.add(ipfsPath, [options])`
> Adds an IPFS object to the pinset and also stores it to the IPFS repo. pinset is the set of hashes currently pinned (not gc'able)
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| source | [CID][] or `string` | A CID or IPFS Path to pin in your repo |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| recursive | `boolean` | `true` | Recursively pin all links contained by the object |
| timeout | `number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| [CID][] | The CID that was pinned |
### Example
```JavaScript
const cid of ipfs.pin.add(CID.parse('QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u'))
console.log(cid)
// Logs:
// CID('QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u')
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.pin.addAll(source, [options])`
> Adds multiple IPFS objects to the pinset and also stores it to the IPFS repo. pinset is the set of hashes currently pinned (not gc'able)
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| source | `AsyncIterable<{ cid: CID, path: string, recursive: boolean, comments: string }>` | One or more CIDs or IPFS Paths to pin in your repo |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields the CIDs that were pinned |
Each yielded object has the form:
```JavaScript
{
cid: CID('QmHash')
}
```
### Example
```JavaScript
for await (const cid of ipfs.pin.addAll(CID.parse('QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u'))) {
console.log(cid)
}
// Logs:
// CID('QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u')
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.pin.ls([options])`
> List all the objects pinned to local storage
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| paths | [CID][] or `Array` or `string` or `Array` | CIDs or IPFS paths to search for in the pinset |
| type | `string` | `undefined` | Filter by this type of pin ("recursive", "direct" or "indirect") |
| timeout | `number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable<{ cid: CID, type: string }>` | An async iterable that yields currently pinned objects with `cid` and `type` properties. `cid` is a [CID][cid] of the pinned node, `type` is the pin type ("recursive", "direct" or "indirect") |
### Example
```JavaScript
for await (const { cid, type } of ipfs.pin.ls()) {
console.log({ cid, type })
}
// { cid: CID(Qmc5XkteJdb337s7VwFBAGtiaoj2QCEzyxtNRy3iMudc3E), type: 'recursive' }
// { cid: CID(QmZbj5ruYneZb8FuR9wnLqJCpCXMQudhSdWhdhp5U1oPWJ), type: 'indirect' }
// { cid: CID(QmSo73bmN47gBxMNqbdV6rZ4KJiqaArqJ1nu5TvFhqqj1R), type: 'indirect' }
```
```JavaScript
for await (const { cid, type } of ipfs.pin.ls({
paths: [ CID.parse('Qmc5..'), CID.parse('QmZb..'), CID.parse('QmSo..') ]
})) {
console.log({ cid, type })
}
// { cid: CID(Qmc5XkteJdb337s7VwFBAGtiaoj2QCEzyxtNRy3iMudc3E), type: 'recursive' }
// { cid: CID(QmZbj5ruYneZb8FuR9wnLqJCpCXMQudhSdWhdhp5U1oPWJ), type: 'indirect' }
// { cid: CID(QmSo73bmN47gBxMNqbdV6rZ4KJiqaArqJ1nu5TvFhqqj1R), type: 'indirect' }
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.pin.rm(ipfsPath, [options])`
> Unpin this block from your repo
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| ipfsPath | [CID][] of string | Unpin this CID or IPFS Path |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| recursive | `boolean` | `true` | Recursively unpin the object linked |
| timeout | `number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| [CID][] | The CIDs that was unpinned |
### Example
```JavaScript
const cid of ipfs.pin.rm(CID.parse('QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u'))
console.log(cid)
// prints the CID that was unpinned
// CID('QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u')
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.pin.rmAll(source, [options])`
> Unpin one or more blocks from your repo
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| source | [CID][], string or `AsyncIterable<{ cid: CID, path: string, recursive: boolean }>` | Unpin this CID |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields the CIDs that were unpinned |
### Example
```JavaScript
for await (const cid of ipfs.pin.rmAll(CID.parse('QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u'))) {
console.log(cid)
}
// prints the CIDs that were unpinned
// CID('QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u')
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.pin.remote.service.add(name, options)`
> Registers remote pinning service with a given name. Errors if service with the given name is already registered.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| name | `string` | Service name |
### Options
An object which must contain following fields:
| Name | Type | Description |
| ---- | ---- | ----------- |
| endpoint | `string` | Service endpoint URL |
| key | `string` | Service key |
An object may have the following optional fields:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| ---- | -------- |
| Promise | Resolves if added successfully, or fails with error e.g. if service with such name is already registered |
### Example
```JavaScript
await ipfs.pin.remote.service.add('pinata', {
endpoint: new URL('https://api.pinata.cloud'),
key: 'your-pinata-key'
})
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.pin.remote.service.ls([options])`
> List registered remote pinning services.
### Options
An object may have the following optional fields:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| stat | `boolean` | `false` | If `true` will include service stats. |
| timeout | `number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| ---- | -------- |
| Promise<[RemotePinService][][]> | List of registered services |
#### `RemotePinService`
Object contains following fields:
| Name | Type | Description |
| ---- | ---- | -------- |
| service | `string` | Service name |
| endpoint | `URL` | Service endpoint URL |
| stat | [Stat][] | Is included only when `stat: true` option was passed |
#### `Stat`
If stats could not be fetched from service (e.g. endpoint was unreachable) object has following form:
| Name | Type | Description |
| ---- | ---- | -------- |
| status | `'invalid'` | Service status |
If stats were fetched from service successfully object has following form:
| Name | Type | Description |
| ---- | ---- | -------- |
| status | `'valid'` | Service status |
| pinCount | [PinCount][] | Pin counts |
#### `PinCount`
Object has following fields:
| Name | Type | Description |
| ---- | ---- | ----------- |
| queued | `number` | Number of queued pins |
| pinning | `number` | Number of pins that are pinning |
| pinned | `number` | Number of pinned pins |
| failed | `number` | Number of faield pins |
### Example
```JavaScript
await ipfs.pin.remote.service.ls()
// [{
// service: 'pinata'
// endpoint: new URL('https://api.pinata.cloud'),
// }]
await ipfs.pin.remote.service.ls({ stat: true })
// [{
// service: 'pinata'
// endpoint: new URL('https://api.pinata.cloud'),
// stat: {
// status: 'valid',
// pinCount: {
// queued: 0,
// pinning: 0,
// pinned: 1,
// failed: 0,
// }
// }
// }]
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.pin.remote.service.rm(name, [options])`
> Unregisteres remote pinning service with a given name (if service with such name is regisetered).
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| name | `string` | Service name |
### Options
An object may have the following optional fields:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| ---- | -------- |
| Promise | Resolves on completion |
### Example
```JavaScript
await ipfs.pin.remote.service.rm('pinata')
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.pin.remote.add(cid, [options])`
> Pin a content with a given CID to a remote pinning service
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| cid | [CID][] | A CID to pin on a remote pinning service |
### Options
An object which must contain following fields:
| Name | Type | Description |
| ---- | ---- | ----------- |
| service | `string` | Name of the remote pinning service to use |
An object may have the following optional fields:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| name | `string` | `undefined` | Name for pinned data; can be used for lookups later (max 255 characters) |
| origins | `Multiaddr[]` | `undefined` | List of multiaddrs known to provide the data (max 20) |
| background | `boolean` | `false` | If true, will add to the queue on the remote service and return immediately. If false or omitted will wait until pinned on the remote service |
| timeout | `number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| ---- | -------- |
| [Pin][] | Pin Object |
#### `Pin`
Object has following fields:
| Type | Description |
| ---- | ----------- |
| [Status][] | Pin status |
| [CID][] | CID of the content |
| `string | undefined` | name that was given to the pin, or `undefined` if no name was not given |
#### `Status`
Status is one of the following string values:
`'queued'`, `'pinning'`, `'pinned'`, `'failed'`
### Example
```JavaScript
const cid = CID.parse('QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u')
const pin = await ipfs.pin.remote.add(cid, {
service: 'pinata',
name: 'block-party'
})
console.log(pin)
// Logs:
// {
// status: 'pinned',
// cid: CID('QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u'),
// name: 'block-party'
// }
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.pin.remote.ls(options)`
> Returns a list of matching pins on the remote pinning service.
### Options
An object which must contain following fields:
| Name | Type | Description |
| ---- | ---- | ----------- |
| service | `string` | Name of the remote pinning service to use |
An object may have the following optional fields:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| cid | [CID][][] | `undefined` | If provided, will only include pin objects that have a CID from the given set. |
| name | `string` | `undefined` | If passed, will only include pin objects with names that have this name (case-sensitive, exact match). |
| status | [Status][][] | ['pinned'] | Return pin objects for pins that have one of the specified status values |
| timeout | `number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| ---- | -------- |
| AysncIterable<[Pin][]> | Pin Objects |
### Example
```JavaScript
for await (const pin of ipfs.pin.remote.ls({ service: 'pinata' })) {
console.log(pin)
}
// Logs:
// {
// status: 'pinned',
// cid: CID('QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u'),
// name: 'block-party'
// }
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.pin.remote.rm(options)`
> Removes a single matching pin object from the remote pinning service. Will error when multiple pins mtach, to remove all matches `rmAll` should be used instead.
### Options
An object which must contain following fields:
| Name | Type | Description |
| ---- | ---- | ----------- |
| service | `string` | Name of the remote pinning service to use |
An object may also contain following optional fields:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| cid | [CID][][] | `undefined` | If provided, will match pin object(s) that have a CID from the given set. |
| name | `string` | `undefined` | If provided, will match pin object(s) with exact (case-sensitive) name. |
| status | [Status][][] | ['pinned'] | If provided, will match pin object(s) that have a status from the given set. |
| timeout | `number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| ---- | -------- |
| Promise | Succeeds on completion |
### Example
```JavaScript
await ipfs.pin.remote.rm({
service: 'pinata',
name: 'block-party'
})
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.pin.remote.rmAll(options)`
> Removes all the matching pin objects from the remote pinning
service.
### Options
An object which must contain following fields:
| Name | Type | Description |
| ---- | ---- | ----------- |
| service | `string` | Name of the remote pinning service to use |
An object may also contain following optional fields:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| cid | [CID][][] | `undefined` | If provided, will match pin object(s) that have a CID from the given set. |
| name | `string` | `undefined` | If provided, will match pin object(s) with exact (case-sensitive) name. |
| status | [Status][][] | ['pinned'] | If provided, will match pin object(s) that have a status from the given set. |
| timeout | `number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| ---- | -------- |
| Promise | Succeeds on completion |
### Example
```JavaScript
// Delete all non 'pinned' pins
await ipfs.pin.remote.rmAll({
service: 'pinata',
status: ['queued', 'pinning', 'failed']
})
```
A great source of [examples][] can be found in the tests for this API.
[Pin]: #pin
[Status]: #status
[RemotePinService]: #remotepinservice
[Status]: #status
[Stat]: #stat
[PinCount]: #pincount
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/pin
[cid]: https://docs.ipfs.io/concepts/content-addressing
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
================================================
FILE: docs/core-api/PUBSUB.md
================================================
# PubSub API
- [`ipfs.pubsub.subscribe(topic, handler, [options])`](#ipfspubsubsubscribetopic-handler-options)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.pubsub.unsubscribe(topic, handler, [options])`](#ipfspubsubunsubscribetopic-handler-options)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [Notes](#notes)
- [`ipfs.pubsub.publish(topic, data, [options])`](#ipfspubsubpublishtopic-data-options)
- [Returns](#returns-2)
- [Example](#example-2)
- [`ipfs.pubsub.ls([options])`](#ipfspubsublsoptions)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-3)
- [Example](#example-3)
- [`ipfs.pubsub.peers(topic, [options])`](#ipfspubsubpeerstopic-options)
- [Returns](#returns-4)
- [Example](#example-4)
## `ipfs.pubsub.subscribe(topic, handler, [options])`
> Subscribe to a pubsub topic.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| topic | `String` | The topic name |
| handler | `Function<(msg) => {}>` | Event handler which will be called with a message object everytime one is received. The `msg` has the format `{from: PeerId, sequenceNumber: bigint, data: Uint8Array, topicIDs: Array}` |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
### Example
```JavaScript
const topic = 'fruit-of-the-day'
const receiveMsg = (msg) => console.log(new TextDecoder().decode(msg.data))
await ipfs.pubsub.subscribe(topic, receiveMsg)
console.log(`subscribed to ${topic}`)
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.pubsub.unsubscribe(topic, handler, [options])`
> Unsubscribes from a pubsub topic.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| topic | `String` | The topic to unsubscribe from |
| handler | `Function<(msg) => {}>` | The handler to remove |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
### Example
```JavaScript
const topic = 'fruit-of-the-day'
const receiveMsg = (msg) => console.log(msg.toString())
await ipfs.pubsub.subscribe(topic, receiveMsg)
console.log(`subscribed to ${topic}`)
await ipfs.pubsub.unsubscribe(topic, receiveMsg)
console.log(`unsubscribed from ${topic}`)
```
Or removing all listeners:
```JavaScript
const topic = 'fruit-of-the-day'
const receiveMsg = (msg) => console.log(msg.toString())
await ipfs.pubsub.subscribe(topic, receiveMsg);
// Will unsubscribe ALL handlers for the given topic
await ipfs.pubsub.unsubscribe(topic);
```
A great source of [examples][] can be found in the tests for this API.
### Notes
If the `topic` and `handler` are provided, the `handler` will no longer receive updates for the `topic`. This behaves like [EventEmitter.removeListener](https://nodejs.org/dist/latest/docs/api/events.html#events_emitter_removelistener_eventname_listener). If the `handler` is not equivalent to the `handler` provided on `subscribe`, no action will be taken.
If **only** the `topic` param is provided, unsubscribe will remove **all** handlers for the `topic`. This behaves like [EventEmitter.removeAllListeners](https://nodejs.org/dist/latest/docs/api/events.html#events_emitter_removealllisteners_eventname). Use this if you would like to no longer receive any updates for the `topic`.
## `ipfs.pubsub.publish(topic, data, [options])`
> Publish a data message to a pubsub topic.
- `topic: String`
- `data: Uint8Array|String` - The message to send
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
### Example
```JavaScript
const topic = 'fruit-of-the-day'
const msg = new TextEncoder().encode('banana')
await ipfs.pubsub.publish(topic, msg)
// msg was broadcasted
console.log(`published to ${topic}`)
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.pubsub.ls([options])`
> Returns the list of subscriptions the peer is subscribed to.
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An array of topicIDs that the peer is subscribed to |
### Example
```JavaScript
const topics = await ipfs.pubsub.ls()
console.log(topics)
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.pubsub.peers(topic, [options])`
> Returns the peers that are subscribed to one topic.
- `topic: String`
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An array of peer IDs subscribed to the `topic` |
### Example
```JavaScript
const topic = 'fruit-of-the-day'
const peerIds = await ipfs.pubsub.peers(topic)
console.log(peerIds)
```
A great source of [examples][] can be found in the tests for this API.
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/pubsub
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
================================================
FILE: docs/core-api/README.md
================================================
# IPFS Core API
This directory contains the description of the core JS IPFS API. In order to be considered "valid", a JS IPFS core implementation must expose the API described here.
This abstraction allows for different implementations including:
1. Full JavaScript native implementation
2. Delgate implementation that invokes another IPFS implementation (e.g., Kubo)
You can use this loose spec as documentation for consuming the core APIs.
It is broken up into the following sections:
* [BITSWAP.md](BITSWAP.md)
* [BLOCK.md](BLOCK.md)
* [BOOTSTRAP.md](BOOTSTRAP.md)
* [CONFIG.md](CONFIG.md)
* [DAG.md](DAG.md)
* [DHT.md](DHT.md)
* [FILES.md](FILES.md)
* [KEY.md](KEY.md)
* [MISCELLANEOUS.md](MISCELLANEOUS.md)
* [NAME.md](NAME.md)
* [OBJECT.md](OBJECT.md) ([deprecated](https://github.com/ipfs/go-ipfs/issues/7936), use the [DAG API](DAG.md) instead)
* [PIN.md](PIN.md)
* [PUBSUB.md](PUBSUB.md)
* [REFS.md](REFS.md)
* [REPO.md](REPO.md)
* [STATS.md](STATS.md)
* [SWARM.md](SWARM.md)
## History
This API was created based off the [Kubo RPC HTTP API](https://docs.ipfs.io/reference/kubo/rpc/). There is no guarantee they stay fully in sync.
================================================
FILE: docs/core-api/REFS.md
================================================
# Refs API
- [`ipfs.refs(ipfsPath, [options])`](#ipfsrefsipfspath-options)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.refs.local([options])`](#ipfsrefslocaloptions)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
## `ipfs.refs(ipfsPath, [options])`
> Get links (references) from an object.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| ipfsPath | [CID][] or `String` | The object to search for references |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| recursive | `boolean` | `false` | Recursively list references of child nodes |
| unique | `boolean` | `false` | Omit duplicate references from output |
| format | `String` | `''` | output edges with given format. Available tokens: ``, ``, `` |
| edges | `boolean` | `false` | output references in edge format: `" -> "` |
| maxDepth | `Number` | `1` | only for recursive refs, limits fetch and listing to the given depth |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields objects representing the links (references) |
Each yielded object is of the form:
```js
{
ref: string,
err: Error | null
}
```
### Example
```JavaScript
for await (const ref of ipfs.refs(ipfsPath, { recursive: true })) {
if (ref.err) {
console.error(ref.err)
} else {
console.log(ref.ref)
// output: "QmHash"
}
}
```
## `ipfs.refs.local([options])`
> Output all local references (CIDs of all blocks in the blockstore)
Blocks in the blockstore are stored by multihash and not CID so yielded CIDs are v1 CIDs with the 'raw' codec. These may not match the CID originally used to store a given block, though the multihash contained within the CID will.
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields objects representing the links (references) |
Each yielded object is of the form:
```js
{
ref: string,
err: Error | null
}
```
### Example
```JavaScript
for await (const ref of ipfs.refs.local()) {
if (ref.err) {
console.error(ref.err)
} else {
console.log(ref.ref)
// output: "QmHash"
}
}
```
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/files-regular
[b]: https://www.npmjs.com/package/buffer
[cid]: https://docs.ipfs.io/concepts/content-addressing
[blob]: https://developer.mozilla.org/en-US/docs/Web/API/Blob
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
================================================
FILE: docs/core-api/REPO.md
================================================
# Repo API
- [`ipfs.repo.gc([options])`](#ipfsrepogcoptions)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.repo.stat([options])`](#ipfsrepostatoptions)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [Notes](#notes)
- [`ipfs.repo.version([options])`](#ipfsrepoversionoptions)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-2)
- [Example](#example-2)
## `ipfs.repo.gc([options])`
> Perform a garbage collection sweep on the repo.
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| quiet | `boolean` | `false` | Write minimal output |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields objects describing nodes that were garbage collected |
Each yielded object contains the following properties:
- `err` is an `Error` if it was not possible to GC a particular block.
- `cid` is the [CID][cid] of the block that was Garbage Collected.
### Example
```JavaScript
for await (const res of ipfs.repo.gc()) {
console.log(res)
}
```
## `ipfs.repo.stat([options])`
> Get stats for the currently used repo.
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| human | `boolean` | `false` | Return storage numbers in `MiB` |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An object containing the repo's info |
the returned object has the following keys:
- `numObjects` is a [BigInt][1].
- `repoSize` is a [BigInt][1], in bytes.
- `repoPath` is a string.
- `version` is a string.
- `storageMax` is a [BigInt][1].
### Example
```JavaScript
const stats = await ipfs.repo.stat()
console.log(stats)
// { numObjects: 15,
// repoSize: 64190,
// repoPath: 'C:\\Users\\henri\\AppData\\Local\\Temp\\ipfs_687c6eb3da07d3b16fe3c63ce17560e9',
// version: 'fs-repo@6',
// storageMax: 10000000000 }
```
### Notes
`stats.repo` and `repo.stat` can be used interchangeably.
## `ipfs.repo.version([options])`
> Show the repo version.
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | A String containing the repo's version |
### Example
```JavaScript
const version = await ipfs.repo.version()
console.log(version)
// "6"
```
[1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
[cid]: https://docs.ipfs.io/concepts/content-addressing
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
================================================
FILE: docs/core-api/STATS.md
================================================
# Stats API
- [`ipfs.stats.bitswap([options]`](#ipfsstatsbitswapoptions)
- [`ipfs.stats.repo([options])`](#ipfsstatsrepooptions)
- [`ipfs.stats.bw([options])`](#ipfsstatsbwoptions)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
## `ipfs.stats.bitswap([options]`
> Show diagnostic information on the bitswap agent.
Note: `stats.bitswap` and `bitswap.stat` can be used interchangeably. See [`bitswap.stat`](./BITSWAP.md#bitswapstat) for more details.
## `ipfs.stats.repo([options])`
> Get stats for the currently used repo.
Note: `stats.repo` and `repo.stat` can be used interchangeably. See [`repo.stat`](./REPO.md#repostat) for more details.
## `ipfs.stats.bw([options])`
> Get IPFS bandwidth information.
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| peer | [PeerId][] | `undefined` | Specifies a peer to print bandwidth for |
| proto | `String` | `undefined` | Specifies a protocol to print bandwidth for |
| poll | `boolean` | `undefined` | Is used to yield bandwidth info at an interval |
| interval | `Number` | `undefined` | The time interval to wait between updating output, if `poll` is `true` |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `AsyncIterable` | An async iterable that yields IPFS bandwidth information |
Each yielded object contains the following keys:
- `totalIn` - is a [BigInt][bigNumber], in bytes.
- `totalOut` - is a [BigInt][bigNumber], in bytes.
- `rateIn` - is a `float`, in bytes.
- `rateOut` - is a `float`, in bytes.
### Example
```JavaScript
for await (const stats of ipfs.stats.bw()) {
console.log(stats)
}
// { totalIn: BigInt {...},
// totalOut: BigInt {...},
// rateIn: number {...},
// rateOut: number {...} }
```
A great source of [examples][] can be found in the tests for this API.
[bigNumber]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/stats
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
[cid]: https://docs.ipfs.io/concepts/content-addressing
[peerid]: https://docs.libp2p.io/concepts/peer-id/
================================================
FILE: docs/core-api/SWARM.md
================================================
# Swarm API
- [Swarm API](#swarm-api)
- [`ipfs.swarm.addrs([options])`](#ipfsswarmaddrsoptions)
- [Parameters](#parameters)
- [Options](#options)
- [Returns](#returns)
- [Example](#example)
- [`ipfs.swarm.connect(addr, [options])`](#ipfsswarmconnectaddr-options)
- [Parameters](#parameters-1)
- [Options](#options-1)
- [Returns](#returns-1)
- [Example](#example-1)
- [`ipfs.swarm.disconnect(addr, [options])`](#ipfsswarmdisconnectaddr-options)
- [Parameters](#parameters-2)
- [Options](#options-2)
- [Returns](#returns-2)
- [Example](#example-2)
- [`ipfs.swarm.localAddrs([options])`](#ipfsswarmlocaladdrsoptions)
- [Parameters](#parameters-3)
- [Options](#options-3)
- [Returns](#returns-3)
- [Example](#example-3)
- [`ipfs.swarm.peers([options])`](#ipfsswarmpeersoptions)
- [Parameters](#parameters-4)
- [Options](#options-4)
- [Returns](#returns-4)
- [Example](#example-4)
## `ipfs.swarm.addrs([options])`
> List of known addresses of each peer connected.
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise>` | A promise that resolves to an array of objects with `id` and `addrs`. `id` is a String - the peer's ID and `addrs` is an array of [Multiaddr](https://github.com/multiformats/js-multiaddr/) - addresses for the peer. |
### Example
```JavaScript
const peerInfos = await ipfs.swarm.addrs()
peerInfos.forEach(info => {
console.log(info.id)
/*
QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt
*/
info.addrs.forEach(addr => console.log(addr.toString()))
/*
/ip4/147.75.94.115/udp/4001/quic
/ip6/2604:1380:3000:1f00::1/udp/4001/quic
/dnsaddr/bootstrap.libp2p.io
/ip6/2604:1380:3000:1f00::1/tcp/4001
/ip4/147.75.94.115/tcp/4001
*/
})
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.swarm.connect(addr, [options])`
> Open a connection to a given address.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| addr | [MultiAddr][] or [PeerId][] | The PeerId or Multiaddr to connect to |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
### Example
```JavaScript
await ipfs.swarm.connect(addr)
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.swarm.disconnect(addr, [options])`
> Close a connection on a given address.
### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| addr | [MultiAddr][] or [PeerId][] | The PeerId or Multiaddr to disconnect from |
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | If action is successfully completed. Otherwise an error will be thrown |
### Example
```JavaScript
await ipfs.swarm.disconnect(addr)
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.swarm.localAddrs([options])`
> Local addresses this node is listening on.
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An array of [`Multiaddr`](https://github.com/multiformats/js-multiaddr) representing the local addresses the node is listening |
### Example
```JavaScript
const multiAddrs = await ipfs.swarm.localAddrs()
console.log(multiAddrs)
```
A great source of [examples][] can be found in the tests for this API.
## `ipfs.swarm.peers([options])`
> List out the peers that we have connections with.
### Parameters
None
### Options
An optional object which may have the following keys:
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| direction | `boolean` | `false` | If true, return connection direction information |
| streams | `boolean` | `false` | If true, return information about open muxed streams |
| verbose | `boolean` | `false` | If true, return all extra information |
| latency | `boolean` | `false` | If true, return latency information |
| timeout | `Number` | `undefined` | A timeout in ms |
| signal | [AbortSignal][] | `undefined` | Can be used to cancel any long running requests started as a result of this call |
### Returns
| Type | Description |
| -------- | -------- |
| `Promise` | An array with the list of peers that the node have connections with |
The returned array has the following form:
- `addr: Multiaddr`
- `peer: String`
- `latency: String` - Only if `verbose: true` was passed
- `muxer: String` - The type of stream muxer the peer is usng
- `streams: string[]` - Only if `verbose: true`, a list of currently open streams
- `direction: number` - Inbound or outbound connection
If an error occurs trying to create an individual object, it will have the properties:
- `error: Error` - the error that occurred
- `rawPeerInfo: Object` - the raw data for the peer
All other properties may be `undefined`.
### Example
```JavaScript
const peerInfos = await ipfs.swarm.peers()
console.log(peerInfos)
```
A great source of [examples][] can be found in the tests for this API.
[examples]: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/src/swarm
[AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
[MultiAddr]: https://github.com/multiformats/js-multiaddr
[peerid]: https://docs.libp2p.io/concepts/peer-id/
================================================
FILE: docs/img/architecture.txt
================================================
┌─────────────────────────────────────────────────────────────────────────────┐
│ The IPFS Architecture │
└─────────────────────────────────────────────────────────────────────────────┘
┏━ ━━ ━━ ━━ ━━ ━━ ━━ ━━ ━━ ━━ ━━ ━━ ━━
======================= IPFS Daemon ======================= ┃
┃ ┃
┃┌────┐ ┏ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━
│ │ ++++++++++++++++++ IPFS Core ++++++++++++++++++ ┃
│ │ ┃ ┌──────────────────────────────────────────────────┐
│HTTP│ ┌─│ API (Core API) │ ┃┃
┃│Gate│ │ ├──────┬──────┬──────┬──────┬──────┬───────┬───────┤ ┃
┃│way │◀┤ │ Repo │Block │ DAG │ Pin │Files │ │Network│ ┃
│ │ │ └──────┴──────┴──────┴──────┴──────┘ └───────┘
│ │ │ │ │ │ │ │ │ ┃
│ │ │ │ │ ┌────┘ │ ┌────┘ ┌────┘ ┃
┃└────┘ │ ┌──┘ │ │ ┌──────┘ │ ▼ ┃┃
┃ │ │┌────────┘ │ ▼ ▼ ┌────────────────────┐
┌────┐ │ ││ │┌───────┐┌──────┐│ libp2p │┃
│ │ │ ││ ││Pinning││Unixfs││ (Network, PubSub, │
│ │ │ ││ ││Service││Engine││ Swarm, Crypto) │┃┃
┃│ │ │ ││ │└───────┘└──────┘│┌──────────────────┐│ ┃
┃│HTTP│ │ ││ │ │ │ ││Connection Manager││┃
│RPC │ │ ││ ├────┴────────┘ │└──────────────────┘│
┌───┐┌────────┐ │API │◀┘ ││ │ │┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ │┃
│CLI││ipfs-api│ │ │ ┃ ││ │ │ Peer Reputation ││ ┃
└───┘└────────┘┃│ │ ││ │ │└ ─ ─ ─ ─ ─ ─ ─ ─ ─ │┃┃
┃│ │ ┃ ││ ┌──┘ └────────────────────┘
│ │ ││ │ ┌ ─ ─ ─ ─ ┐┌ ─ ─ ─ ─ ─ ┃
└────┘ ┃ ││ │ Providers GC │
││ ▼ │ Service ││ ┃┃
┃ ┃ ││┌─────────────┐ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ┃
┃ │││Graph Service│─────┬───────────┬───────────┐ ┃
┃ ││└─────────────┘ ▼ ▼ ▼
││ │ ┌ ─ ─ ─ ─ ─ ┌ ─ ─ ─ ─ ─ ┌ ─ ─ ─ ─ ─ ┃
┃ │└───────┤ GraphSync │ GraphSyncB│ GraphSyncC│ ┃
┃ │ ▼ └ ─ ─ ─ ─ ─ └ ─ ─ ─ ─ ─ └ ─ ─ ─ ─ ─ ┃┃
┃ ┃ │ ┌─────────────┐
│ │Block Service│─────┬───────────┬───────────┐ ┃
┃ │ └─────────────┘ ▼ ▼ ▼
│ │ ┌──────────┐┌ ─ ─ ─ ─ ─ ┌ ─ ─ ─ ─ ─ ┃┃
┃ ┃ └─────┬──┴──────│ Bitswap │ BitswapB │ BitswapB │ ┃
┃ ▼ └──────────┘└ ─ ─ ─ ─ ─ └ ─ ─ ─ ─ ─ ┃
┃ ┌─────────┐
│ Repo │ ┃
┃ └─────────┘ ┃
┃ │ ┃┃
┃ ┃ ┌─┴──────┬──────────┬───────┐
▼ ▼ ▼ ▼ ┃
┃ ┌────┐┌──────────┐┌────────┐┌────┐
│ fs ││indexedDB ││LevelDB ││ S3 │ ┃┃
┃ ┃ └────┘└──────────┘└────────┘└────┘ ┃
┃ ┃
┗ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━ ━
━━ ━━ ━━ ━━ ━━ ━━ ━━ ━━ ━━ ━━ ━━ ━━ ━━
┌───────────────────────────────────────────────────────────────────────────┐
│ Legend │
│ ┌ ─ ─ ┐ │
│ Planned, not yet implemented │
│ └ ─ ─ ┘ │
│ ┌─────┐ │
│ │ │ Exist and shipped with IPFS │
│ └─────┘ │
└───────────────────────────────────────────────────────────────────────────┘
================================================
FILE: docs/img/core.txt
================================================
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ IPFS Core │
│ │
├ ─ ─ ─ ─┌ ─ ─ ─ ─┌ ─ ─ ─ ─┌ ─ ─ ─ ─┌ ─ ─ ─ ─ ┌ ─ ─ ─ ─│
│ Repo │ Block │Bitswap │ DAG │ Files │ Swarm │
│ │ │ │ │ │ │
└────────┴───┬────┴────────┴────────┴────────┴────────────────────────────────┘
│ │ │ │ │ │
│ │ │ │ ▼ │
│ │ │ │ ┌──────────────────┐ ▼
│ │ │ │ │ipfs-unixfs-engine│ ┌─────────────────────┐
│ │ │ │ └──────┬───────────┤ │ │
│ │ │ │ │ │ipfs-unixfs│ │ libp2p │
│ │ │ │ │ └───────────┘ │ │
│ │ │ ▼ ▼ └─────────────────────┘
│ │ │ ┌─────────────┬────────┐
│ │ │ │ipfs-resolver│dag-pb │
│ │ │ └─────────────┼────────┤
│ ▼ │ │ │dag-cbor│
│ ┌─────────────┴─────┐ │ ├────────┤
│ │ipfs-blocks-service│◀─────┘ │ethereum│
│ └─────────────┬─────┘ ├────────┤
│ │ │ │ │... │
│ │ │ │ └────────┘
│ │ │ │
│ │ ▼ ▼
│ │ ┌────────────┐
├───────┴──│ipfs-bitswap│
│ └────────────┘
▼
┌─────────┬─────────┐
│ │ fs │
│ipfs-repo├─────────┤
│ │IndexedDB│
└─────────┴─────────┘
================================================
FILE: docs/img/overview.txt
================================================
offline mode - uses IPFS core directly
┌────────────────────────────────────────────┐
│ │
│ │
│ online mode - uses IPFS through http-api │
┌────────────┐ │ ┌─────────────┐ │ ┌─────────┐
│ │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ │ │ │
│ CLI │───┴── ipfs-http-client ├──▶│ HTTP-API │───┴───▶│IPFS Core│
│ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ │ │
└────────────┘ └─────────────┘ └─────────┘
△ △ △
├───────────────────────────────────────────┴────────────────────┘
│
┌────────────┐
│ Tests │
└────────────┘
================================================
FILE: docs/upgrading/v0.62-v0.63.md
================================================
# Migrating to ipfs@0.63 and ipfs-core@0.15
> A migration guide for refactoring your application code from `ipfs@0.62.x` to `ipfs@0.63.x`
## Table of Contents
- [ESM](#esm)
- [TypeScript and ESM](#typescript-and-esm)
- [`libp2p@0.37.x`](#libp2p037x)
- [PeerIds](#peerids)
- [multiaddrs](#multiaddrs)
## ESM
The biggest change to `ipfs@0.63.x` is that the module is now [ESM-only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).
ESM is the module system for JavaScript. It allows us to structure our code in separate files without polluting a global namespace.
Other systems have tried to fill this gap, notably CommonJS, AMD, RequireJS and others, but ESM is [the official standard format](https://tc39.es/ecma262/#sec-modules) to package JavaScript code for reuse.
If you see errors similar to `Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in node_modules/ipfs/package.json` you are likely trying to load ESM code from a CJS environment via `require`. This is not possible, instead it must be loaded using `import`.
If your application is not yet ESM or you are not ready to port it to ESM, you can use the [dynamic `import` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) to load `ipfs` at runtime from a CJS module:
```js
async function loadIpfs () {
const { create } = await import('ipfs-core')
const node = await create({
// ... config here
})
return node
}
```
### TypeScript and ESM
When authoring typescript it can often look like you are writing ESM:
```ts
import { create } from 'ipfs-core'
create()
```
When this is transpiled to JavaScript the default settings will emit CJS which will fail at runtime:
```js
"use strict";
exports.__esModule = true;
var ipfs_core_1 = require("ipfs-core");
(0, ipfs_core_1.create)();
```
You may also see errors about private identifiers:
```console
node_modules/@libp2p/interfaces/dist/src/events.d.ts:19:5 - error TS18028: Private identifiers are only available when targeting ECMAScript 2015 and higher.
19 #private;
~~~~~~~~
```
To build correctly with ESM as a target, update your `tsconfig.json` to include the following:
```js
{
"module": "es2020", // ensures output is ESM
"target": "es2020", // support modern features like private identifiers
// other settings
}
```
They must both be set to `es2020` at least, more recent versions will also work.
If in doubt, examine the JavaScript files `tsc` emits and ensure that any `ipfs` modules are being loaded with `import` and not `require`.
## `libp2p@0.37.x`
`ipfs@0.63.x` upgrades to `libp2p@0.37.x`. This is a significant refactor that ports the entire stack to TypeScript and publishes all modules as ESM-only code.
Please see the [libp2p 0.37.x upgrade guide](https://github.com/libp2p/js-libp2p/blob/master/doc/migrations/v0.36-v0.37.md) for how this may affect your application.
## PeerIds
The core `libp2p` module and all supporting modules have now been ported to TypeScript in a complete ground-up rewrite. We took this opportunity to solve a few long-standing problems with some of the data types, particularly in how they relate to use in the browser.
One problem we have solved is that the `PeerId` objects used internally expose some cryptographic operations that require heavyweight libraries to be included in browser bundles due to there being no native web-crypto implementation of the algorithms used in those operations.
With `libp2p@0.37.x` those operations have been encapsulated in the `@libp2p/crypto` module which means `PeerId` objects become a lot more lightweight and can now be exposed/accepted as core-api types so we can use them to differentiate between different data types instead of having to treat everything as strings.
The affected methods are:
```js
// `peerId` must now be a `PeerId`, previously it was a `string`
ipfs.bitswap.wantlistForPeer(peerId, options)
// Bitswp peers are now returned as `PeerId[]` instead of `string[]`
ipfs.bitswap.stat(options)
// `peerId` must now be a `PeerId`
ipfs.dht.findPeer(peerId, options)
// `peerIdOrCid` must now be a `PeerId` or a `CID`, previously it was a `string` or a `CID`
ipfs.dht.query(peerIdOrCid, options)
// the following DHT events have their `from` field as `PeerId`, previously it was a `string`
PeerResponseEvent
ValueEvent
DialingPeerEvent
// the following DHT events have had their `from` property removed because it is not exposed by go-ipfs so causes incompatibilities
QueryErrorEvent
FinalPeerEvent
// the folloing DHT events have had their `to` property removed because it is not exposed by go-ipfs so causes incompatibilities
SendingQueryEvent
// the `providers` and `closer` properties (where applicable) of the following events have the `peerId` property specified as a `PeerId`, previously it was a `string`
PeerResponseEvent
PeerResponseEvent
// `value` can now be a string or a `PeerId`. If a string is passed it will be interpreted as a DNS address.
ipfs.name.resolve(value, options)
// The return type of this method is now `Promise`, previously it was a `Promise`
ipfs.pubsub.peers(topic, options)
// `peerId` must now be a `PeerId`, previously it was a `string`
ipfs.ping(peerId, options)
// the `peer` property of `options` must now be a `PeerId` when specified, previously it was a `string`
ipfs.stats.bw(options)
// `multiaddrOrPeerId` must be a `Multiaddr` or `PeerId`, previously it was a `Multiaddr` or `string`
ipfs.swarm.connect(multiaddrOrPeerId, options)
// `multiaddrOrPeerId` must be a `Multiaddr` or `PeerId`, previously it was a `Multiaddr` or `string`
ipfs.swarm.disconnect(multiaddrOrPeerId, options)
```
`PeerId`s can be created from strings using the `@libp2p/peer-id` module:
```js
import { peerIdFromString } from '@libp2p/peer-id'
const peerId = peerIdFromString('Qmfoo')
```
They can also be created using the `@libp2p/peer-id-factory` module:
```js
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
const peerId = await createEd25519PeerId()
```
## multiaddrs
The `multiaddr` module has been ported to TypeScript and is now published as ESM-only.
It has been renamed to `@multiformats/multiaddr` so please update your dependencies and replace usage in your code.
The API otherwise is compatible.
================================================
FILE: docs/upgrading/v0.63-v0.64.md
================================================
# Migrating to ipfs@0.64 and ipfs-core@0.16
> A migration guide for refactoring your application code from `ipfs@0.63.x` to `ipfs@0.64.x`
## Table of Contents
- [libp2p](#libp2p)
## libp2p
The upgrade to `ipfs@0.64.x` incorporates an update to `libp2p@0.38.x` but no API changes.
If your application uses only the default libp2p config there is nothing to do.
If you supply a custom `libp2p` instance to the `ipfs` factory function you should consult the [`libp2p@0.38.x` upgrade guide](https://github.com/libp2p/js-libp2p/blob/master/doc/migrations/v0.37-v0.38.md) for any changes you need to make.
================================================
FILE: docs/upgrading/v0.64-v0.65.md
================================================
# Migrating to ipfs@0.65 and ipfs-core@0.17
> A migration guide for refactoring your application code from `ipfs@0.64.x` to `ipfs@0.65.x`
## Table of Contents
- [libp2p](#libp2p)
- [multiformats](#multiformats)
## libp2p
The upgrade to `ipfs@0.65.x` incorporates an update to `libp2p@0.40.x` but no API changes.
If your application uses only the default libp2p config there is nothing to do.
If you supply a custom `libp2p` instance to the `ipfs` factory function you should consult the [`libp2p@0.40.x` upgrade guide](https://github.com/libp2p/js-libp2p/blob/master/doc/migrations/v0.39-v0.40.md) for any changes you need to make.
## multiformats
`ipfs@0.65.x` now uses `multiformats@10.x.x`, this means instances of the `CID` class now come from that module and not `multiformats@9.x.x` so any `instanceof` checks your codebase has may break if instances are compare to the class loaded from a different module version.
If your project also has a dependency on the `multiformats` module, it should be updated to `10.x.x` in line with js-ipfs.
================================================
FILE: package-list.json
================================================
{
"columns": [
"Package",
"Version",
"Deps",
"CI/Travis",
"Coverage",
"Lead Maintainer"
],
"rows": [
"Files",
["ipfs/js-ipfs-unixfs", "ipfs-unixfs"],
"Repo",
["ipfs/js-ipfs-repo", "ipfs-repo"],
["ipfs/js-ipfs-repo-migrations", "ipfs-repo-migrations"],
"Exchange",
["ipfs/js-ipfs-bitswap", "ipfs-bitswap"],
"IPNS",
["ipfs/js-ipns", "ipns"],
"Generics/Utils",
["ipfs/js-ipfs", "ipfs-utils"],
["ipfs/js-ipfs", "ipfs-http-client"],
["ipfs/js-ipfs-http-response", "ipfs-http-response"],
["ipfs/js-ipfsd-ctl", "ipfsd-ctl"],
["ipfs/is-ipfs", "is-ipfs"],
["ipfs/aegir", "aegir"],
"libp2p",
["libp2p/js-libp2p", "libp2p"],
["libp2p/js-peer-id", "peer-id"],
["libp2p/js-libp2p-crypto", "libp2p-crypto"],
["libp2p/js-libp2p-floodsub", "libp2p-floodsub"],
["ChainSafe/gossipsub-js", "libp2p-gossipsub"],
["libp2p/js-libp2p-kad-dht", "libp2p-kad-dht"],
["libp2p/js-libp2p-mdns", "libp2p-mdns"],
["libp2p/js-libp2p-bootstrap", "libp2p-bootstrap"],
["ChainSafe/js-libp2p-noise", "libp2p-noise"],
["libp2p/js-libp2p-tcp", "libp2p-tcp"],
["libp2p/js-libp2p-webrtc-star", "libp2p-webrtc-star"],
["libp2p/js-libp2p-websockets", "libp2p-websockets"],
["libp2p/js-libp2p-mplex", "libp2p-mplex"],
["libp2p/js-libp2p-delegated-content-routing", "libp2p-delegated-content-routing"],
["libp2p/js-libp2p-delegated-peer-routing", "libp2p-delegated-peer-routing"],
"IPLD",
["ipld/js-dag-pb", "@ipld/dag-pb"],
["ipld/js-dag-cbor", "@ipld/dag-cbor"],
"Multiformats",
["multiformats/js-multiformats", "multiformats"],
["multiformats/js-mafmt", "mafmt"],
["multiformats/js-multiaddr", "@multiformats/multiaddr"]
]
}
================================================
FILE: package.json
================================================
{
"name": "js-ipfs",
"version": "1.0.0",
"description": "JavaScript implementation of the IPFS specification",
"license": "Apache-2.0 OR MIT",
"homepage": "https://github.com/ipfs/js-ipfs#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/ipfs/js-ipfs.git"
},
"bugs": {
"url": "https://github.com/ipfs/js-ipfs/issues"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=7.0.0"
},
"private": true,
"scripts": {
"reset": "aegir run clean && aegir clean packages/*/node_modules node_modules package-lock.json packages/*/package-lock.json",
"test": "aegir run test",
"test:node": "aegir run test:node",
"test:chrome": "aegir run test:chrome",
"test:chrome-webworker": "aegir run test:chrome-webworker",
"test:firefox": "aegir run test:firefox",
"test:firefox-webworker": "aegir run test:firefox-webworker",
"test:electron-main": "aegir run test:electron-main",
"test:external": "aegir run test:external",
"test:cli": "aegir run test:cli",
"test:interop": "aegir run test:interop",
"test:interface:client": "aegir run test:interface:client",
"test:interface:core": "aegir run test:interface:core",
"test:interface:http-go": "aegir run test:interface:http-go",
"test:interface:http-js": "aegir run test:interface:http-js",
"test:interface:message-port-client": "aegir run test:interface:message-port-client",
"coverage": "aegir run coverage",
"build": "aegir run build",
"clean": "aegir run clean",
"lint": "aegir run lint",
"dep-check": "aegir run dep-check",
"release": "run-s build npm:release docker:release",
"npm:release": "aegir exec npm -- publish",
"docker:release": "run-s docker:release:*",
"docker:release:build": "docker build . --no-cache --tag js-ipfs:latest --file ./Dockerfile.latest",
"docker:release:tag-latest": "docker tag js-ipfs:latest docker.io/ipfs/js-ipfs:latest",
"docker:release:tag-version": "docker tag js-ipfs:latest docker.io/ipfs/js-ipfs:v`npm show ipfs@latest version -q`",
"docker:release:push-latest": "docker push ipfs/js-ipfs:latest",
"docker:release:push-version": "docker push ipfs/js-ipfs:v`npm show ipfs@latest version -q`",
"release:rc": "run-s npm:rc docker:rc",
"npm:rc": "aegir release-rc",
"docker:rc": "run-s docker:rc:*",
"docker:rc:build": "docker build . --no-cache --tag js-ipfs:next --file ./Dockerfile.next",
"docker:rc:tag-next": "docker tag js-ipfs:next docker.io/ipfs/js-ipfs:next",
"docker:rc:tag-rc": "docker tag js-ipfs:next docker.io/ipfs/js-ipfs:v`npm show ipfs@next version -q`",
"docker:rc:push-next": "docker push ipfs/js-ipfs:next",
"docker:rc:push-rc": "docker push ipfs/js-ipfs:v`npm show ipfs@next version -q`"
},
"devDependencies": {
"aegir": "^37.11.0",
"npm-run-all": "^4.1.5"
},
"eslintConfig": {
"extends": "ipfs",
"ignorePatterns": [
"!.aegir.js"
]
},
"workspaces": [
"packages/*"
]
}
================================================
FILE: packages/interface-ipfs-core/.aegir.js
================================================
/** @type {import('aegir').PartialOptions} */
export default {
build: {
bundlesizeMax: '338kB'
}
}
================================================
FILE: packages/interface-ipfs-core/CHANGELOG.md
================================================
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
### [0.158.1](https://www.github.com/ipfs/js-ipfs/compare/interface-ipfs-core-v0.158.0...interface-ipfs-core-v0.158.1) (2023-05-25)
### Bug Fixes
* add deprecation notice to readmes ([#4362](https://www.github.com/ipfs/js-ipfs/issues/4362)) ([7b79c1b](https://www.github.com/ipfs/js-ipfs/commit/7b79c1b8df5c818dc124b346ea28330455732d5c))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core-types bumped from ^0.14.0 to ^0.14.1
## [0.158.0](https://www.github.com/ipfs/js-ipfs/compare/interface-ipfs-core-v0.157.0...interface-ipfs-core-v0.158.0) (2023-01-11)
### ⚠ BREAKING CHANGES
* update multiformats to v11.x.x and related depenendcies (#4277)
### Bug Fixes
* allow reading rawLeaves in MFS ([#4282](https://www.github.com/ipfs/js-ipfs/issues/4282)) ([0cfcaf6](https://www.github.com/ipfs/js-ipfs/commit/0cfcaf65998bdc2af0cc29ac48229bb3bc35c5b8))
* disallow publishing pubsub messages to zero peers ([#4286](https://www.github.com/ipfs/js-ipfs/issues/4286)) ([fa578ba](https://www.github.com/ipfs/js-ipfs/commit/fa578bace93e459849a0ffcebbd6f222dc05652d))
* mfs blob import for files larger than 262144b ([#4251](https://www.github.com/ipfs/js-ipfs/issues/4251)) ([6be5906](https://www.github.com/ipfs/js-ipfs/commit/6be59068cc99c517526bfa123ad475ae05fcbaef)), closes [#3601](https://www.github.com/ipfs/js-ipfs/issues/3601) [#3576](https://www.github.com/ipfs/js-ipfs/issues/3576)
* update multiformats to v11.x.x and related depenendcies ([#4277](https://www.github.com/ipfs/js-ipfs/issues/4277)) ([521c84a](https://www.github.com/ipfs/js-ipfs/commit/521c84a958b04d61702577a5adce28519c1b2a3b))
* use aegir to publish RCs ([#4284](https://www.github.com/ipfs/js-ipfs/issues/4284)) ([6d90cbf](https://www.github.com/ipfs/js-ipfs/commit/6d90cbf321a7dbf4b1084ba20f0c514dc08d8d0a))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core-types bumped from ^0.13.0 to ^0.14.0
## [0.157.0](https://www.github.com/ipfs/js-ipfs/compare/interface-ipfs-core-v0.156.1...interface-ipfs-core-v0.157.0) (2022-10-24)
### ⚠ BREAKING CHANGES
* ipfs is now bundled with libp2p@0.40.x which has different config
* require IPNS V2 signatures (#4207)
### Features
* upgrade libp2p to 0.40.x ([#4237](https://www.github.com/ipfs/js-ipfs/issues/4237)) ([0cee4a4](https://www.github.com/ipfs/js-ipfs/commit/0cee4a4c55767022584dcbade0b0b9b43326f9c9))
### Bug Fixes
* require IPNS V2 signatures ([#4207](https://www.github.com/ipfs/js-ipfs/issues/4207)) ([d1b0a8a](https://www.github.com/ipfs/js-ipfs/commit/d1b0a8a71073b4ece0dbda5a5405d76dd8d5b358))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core-types bumped from ^0.12.1 to ^0.13.0
### [0.156.1](https://www.github.com/ipfs/js-ipfs/compare/interface-ipfs-core-v0.156.0...interface-ipfs-core-v0.156.1) (2022-09-21)
### Bug Fixes
* update @multiformats/multiadd to 11.0.0 ([2a830bf](https://www.github.com/ipfs/js-ipfs/commit/2a830bf58a5929fcce51dede871c99f62192fbda))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core-types bumped from ^0.12.0 to ^0.12.1
## [0.156.0](https://www.github.com/ipfs/js-ipfs/compare/interface-ipfs-core-v0.155.2...interface-ipfs-core-v0.156.0) (2022-09-06)
### ⚠ BREAKING CHANGES
* update to libp2p@0.38.x (#4151)
### deps
* update to libp2p@0.38.x ([#4151](https://www.github.com/ipfs/js-ipfs/issues/4151)) ([39dbf70](https://www.github.com/ipfs/js-ipfs/commit/39dbf708ec31b263115e44f420651fa4e056a89e))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core-types bumped from ^0.11.0 to ^0.12.0
### [0.155.2](https://www.github.com/ipfs/js-ipfs/compare/interface-ipfs-core-v0.155.1...interface-ipfs-core-v0.155.2) (2022-06-24)
### Bug Fixes
* make pubsub message types consistent ([#4145](https://www.github.com/ipfs/js-ipfs/issues/4145)) ([00bd3dd](https://www.github.com/ipfs/js-ipfs/commit/00bd3dd0bca7fc705e5e87272972f586d1f161e8))
### [0.155.1](https://www.github.com/ipfs/js-ipfs/compare/interface-ipfs-core-v0.155.0...interface-ipfs-core-v0.155.1) (2022-06-22)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core-types bumped from ^0.11.0 to ^0.11.1
## [0.155.0](https://www.github.com/ipfs/js-ipfs/compare/interface-ipfs-core-v0.154.3...interface-ipfs-core-v0.155.0) (2022-05-27)
### ⚠ BREAKING CHANGES
* This module is now ESM only and there return types of some methods have changed
### Features
* update to libp2p 0.37.x ([#4092](https://www.github.com/ipfs/js-ipfs/issues/4092)) ([74aee8b](https://www.github.com/ipfs/js-ipfs/commit/74aee8b3d78f233c3199a3e9a6c0ac628a31a433))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core-types bumped from ^0.10.3 to ^0.11.0
### [0.154.3](https://www.github.com/ipfs/js-ipfs/compare/interface-ipfs-core-v0.154.2...interface-ipfs-core-v0.154.3) (2022-04-20)
### Bug Fixes
* update car dependency for CARv2 read support ([#4085](https://www.github.com/ipfs/js-ipfs/issues/4085)) ([c367840](https://www.github.com/ipfs/js-ipfs/commit/c367840062e3fc555e696e4fc621651ed1929213))
* upgrade dep of ipfs-utils ^9.0.2->^9.0.6 ([#4086](https://www.github.com/ipfs/js-ipfs/issues/4086)) ([8f7ce23](https://www.github.com/ipfs/js-ipfs/commit/8f7ce23c18be12bdc52b98bfccbd0a5a2a9c9f7e)), closes [#4080](https://www.github.com/ipfs/js-ipfs/issues/4080)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core-types bumped from ^0.10.2 to ^0.10.3
### [0.154.2](https://www.github.com/ipfs/js-ipfs/compare/interface-ipfs-core-v0.154.1...interface-ipfs-core-v0.154.2) (2022-03-01)
### Bug Fixes
* missing files on publish ([#4056](https://www.github.com/ipfs/js-ipfs/issues/4056)) ([125d42b](https://www.github.com/ipfs/js-ipfs/commit/125d42ba72f905bf95b66489c1b593cbf0a623cb)), closes [#3976](https://www.github.com/ipfs/js-ipfs/issues/3976)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core-types bumped from ^0.10.1 to ^0.10.2
### [0.154.1](https://www.github.com/ipfs/js-ipfs/compare/interface-ipfs-core-v0.154.0...interface-ipfs-core-v0.154.1) (2022-02-06)
### Bug Fixes
* **dag:** replace custom dag walk with multiformats/traversal ([#3950](https://www.github.com/ipfs/js-ipfs/issues/3950)) ([596b1f4](https://www.github.com/ipfs/js-ipfs/commit/596b1f48a014083b1736e4ad7e746c652d2583b1))
* override hashing algorithm when importing files ([#4042](https://www.github.com/ipfs/js-ipfs/issues/4042)) ([709831f](https://www.github.com/ipfs/js-ipfs/commit/709831f61a822d28a6b8e4d6ddc2b659a836079f)), closes [#3952](https://www.github.com/ipfs/js-ipfs/issues/3952)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core-types bumped from ^0.10.0 to ^0.10.1
## [0.154.0](https://www.github.com/ipfs/js-ipfs/compare/interface-ipfs-core-v0.153.0...interface-ipfs-core-v0.154.0) (2022-01-27)
### ⚠ BREAKING CHANGES
* peerstore methods are now all async, the repo is migrated to v12
* node 15+ is required
### Features
* add support for dag-jose codec ([#4028](https://www.github.com/ipfs/js-ipfs/issues/4028)) ([fbe1492](https://www.github.com/ipfs/js-ipfs/commit/fbe1492395ad98e620a872208530a3f8f61535a9))
* libp2p async peerstore ([#4018](https://www.github.com/ipfs/js-ipfs/issues/4018)) ([a6b201a](https://www.github.com/ipfs/js-ipfs/commit/a6b201af2c3697430ab0ebe002dd573d185f1ac0))
### Bug Fixes
* remove abort-controller deps ([#4015](https://www.github.com/ipfs/js-ipfs/issues/4015)) ([902e887](https://www.github.com/ipfs/js-ipfs/commit/902e887e1acac87f607324fa7cb5ad4b14aefcf3))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core-types bumped from ^0.9.0 to ^0.10.0
## [0.153.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.152.2...interface-ipfs-core@0.153.0) (2021-12-15)
### Bug Fixes
* **pubsub:** multibase in pubsub http rpc ([#3922](https://github.com/ipfs/js-ipfs/issues/3922)) ([6eeaca4](https://github.com/ipfs/js-ipfs/commit/6eeaca452c36fa13be42d704575c577e4ca938f1))
* return nested value from dag.get ([#3966](https://github.com/ipfs/js-ipfs/issues/3966)) ([45ac973](https://github.com/ipfs/js-ipfs/commit/45ac9730d6484e8324acfbc3579fce052b8452d7)), closes [#3957](https://github.com/ipfs/js-ipfs/issues/3957)
### chore
* Bump @ipld/dag-cbor to v7 ([#3977](https://github.com/ipfs/js-ipfs/issues/3977)) ([73476f5](https://github.com/ipfs/js-ipfs/commit/73476f55e39ecfb01eb2b4880637aad658f51bc2))
### Features
* dht client ([#3947](https://github.com/ipfs/js-ipfs/issues/3947)) ([62d8ecb](https://github.com/ipfs/js-ipfs/commit/62d8ecbc723e693a2544e69172d99c576d187c23))
* update DAG API to match go-ipfs@0.10 changes ([#3917](https://github.com/ipfs/js-ipfs/issues/3917)) ([38c01be](https://github.com/ipfs/js-ipfs/commit/38c01be03b4fd5f401cd9b698cfdb4237d835b01))
### BREAKING CHANGES
* **pubsub:** We had to make breaking changes to `pubsub` commands sent over HTTP RPC to fix data corruption caused by topic names and payload bytes that included `\n`. More details in https://github.com/ipfs/go-ipfs/issues/7939 and https://github.com/ipfs/go-ipfs/pull/8183
* On decode of CBOR blocks, `undefined` values will be coerced to `null`
* `ipfs.dag.put` no longer accepts a `format` arg, it is now `storeCodec` and `inputCodec`. `'json'` has become `'dag-json'`, `'cbor'` has become `'dag-cbor'` and so on
* The DHT API has been refactored to return async iterators of query events
## [0.152.2](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.152.1...interface-ipfs-core@0.152.2) (2021-11-24)
**Note:** Version bump only for package interface-ipfs-core
## [0.152.1](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.152.0...interface-ipfs-core@0.152.1) (2021-11-19)
**Note:** Version bump only for package interface-ipfs-core
## [0.152.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.151.1...interface-ipfs-core@0.152.0) (2021-11-12)
### Bug Fixes
* do not accept single items for ipfs.add ([#3900](https://github.com/ipfs/js-ipfs/issues/3900)) ([04e3cf3](https://github.com/ipfs/js-ipfs/commit/04e3cf3f46b585c4644cba70516f375e95361f52))
* do not lose files when writing files into subshards that contain other subshards ([#3936](https://github.com/ipfs/js-ipfs/issues/3936)) ([8a3ed19](https://github.com/ipfs/js-ipfs/commit/8a3ed19575beaafe5dfd3bce310a548950c148d0)), closes [#3921](https://github.com/ipfs/js-ipfs/issues/3921)
### BREAKING CHANGES
* errors will now be thrown if multiple items are passed to `ipfs.add` or single items to `ipfs.addAll` (n.b. you can still pass a list of a single item to `ipfs.addAll`)
## [0.151.1](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.151.0...interface-ipfs-core@0.151.1) (2021-09-28)
**Note:** Version bump only for package interface-ipfs-core
## [0.151.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.150.4...interface-ipfs-core@0.151.0) (2021-09-24)
### Features
* pull in new globSource ([#3889](https://github.com/ipfs/js-ipfs/issues/3889)) ([be4a542](https://github.com/ipfs/js-ipfs/commit/be4a5428ebc4b05a2edd9a91bf9df6416c1a8c2b))
* switch to esm ([#3879](https://github.com/ipfs/js-ipfs/issues/3879)) ([9a40109](https://github.com/ipfs/js-ipfs/commit/9a40109632e5b4837eb77a2f57dbc77fbf1fe099))
### BREAKING CHANGES
* the globSource api has changed from `globSource(dir, opts)` to `globSource(dir, pattern, opts)`
* There are no default exports and everything is now dual published as ESM/CJS
## [0.150.4](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.150.3...interface-ipfs-core@0.150.4) (2021-09-17)
**Note:** Version bump only for package interface-ipfs-core
## [0.150.3](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.150.2...interface-ipfs-core@0.150.3) (2021-09-17)
**Note:** Version bump only for package interface-ipfs-core
## [0.150.2](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.150.1...interface-ipfs-core@0.150.2) (2021-09-02)
### Bug Fixes
* declare types in .ts files ([#3840](https://github.com/ipfs/js-ipfs/issues/3840)) ([eba5fe6](https://github.com/ipfs/js-ipfs/commit/eba5fe6832858107b3e1ae02c99de674622f12b4))
* remove client-side timeout from http rpc calls ([#3178](https://github.com/ipfs/js-ipfs/issues/3178)) ([f11220e](https://github.com/ipfs/js-ipfs/commit/f11220e00a12afed5ebbbd8b4c5134595aea735d)), closes [#3161](https://github.com/ipfs/js-ipfs/issues/3161)
* remove use of instanceof for CID class ([#3847](https://github.com/ipfs/js-ipfs/issues/3847)) ([ebbb12d](https://github.com/ipfs/js-ipfs/commit/ebbb12db523c53ce8e4ddae5266cd9acb3504431))
## [0.150.1](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.150.0...interface-ipfs-core@0.150.1) (2021-08-25)
**Note:** Version bump only for package interface-ipfs-core
## [0.150.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.149.0...interface-ipfs-core@0.150.0) (2021-08-17)
### Bug Fixes
* pin nanoid version ([#3807](https://github.com/ipfs/js-ipfs/issues/3807)) ([474523a](https://github.com/ipfs/js-ipfs/commit/474523ab8702729f697843d433a7a08baf2d101f))
* throw error on missing input to add/addAll ([#3818](https://github.com/ipfs/js-ipfs/issues/3818)) ([1343708](https://github.com/ipfs/js-ipfs/commit/1343708f70d7298b6677555803d68ff282d89439)), closes [#3788](https://github.com/ipfs/js-ipfs/issues/3788)
### Features
* pubsub over gRPC ([#3813](https://github.com/ipfs/js-ipfs/issues/3813)) ([e7d5509](https://github.com/ipfs/js-ipfs/commit/e7d5509c87e87aed6be3c1d0b2a01ab74cdc1ed9)), closes [#3741](https://github.com/ipfs/js-ipfs/issues/3741)
## [0.149.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.148.0...interface-ipfs-core@0.149.0) (2021-08-11)
### Bug Fixes
* return rate in/out as number ([#3798](https://github.com/ipfs/js-ipfs/issues/3798)) ([2f3df7a](https://github.com/ipfs/js-ipfs/commit/2f3df7a70fe94d6bdf20947854dc9d0b88cb759a)), closes [#3782](https://github.com/ipfs/js-ipfs/issues/3782)
### Features
* ed25519 keys by default ([#3693](https://github.com/ipfs/js-ipfs/issues/3693)) ([33fa734](https://github.com/ipfs/js-ipfs/commit/33fa7341c3baaf0926d887c071cc6fbce5ac49a8))
* make ipfs.get output tarballs ([#3785](https://github.com/ipfs/js-ipfs/issues/3785)) ([1ad6001](https://github.com/ipfs/js-ipfs/commit/1ad60018d39d5b46c484756631e30e1989fd8eba))
### BREAKING CHANGES
* rateIn/rateOut are returned as numbers
* the output type of `ipfs.get` has changed and the `recursive` option has been removed from `ipfs.ls` since it was not supported everywhere
## [0.148.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.147.0...interface-ipfs-core@0.148.0) (2021-07-27)
### Bug Fixes
* fix flaky pubsub test ([#3761](https://github.com/ipfs/js-ipfs/issues/3761)) ([8bcf56f](https://github.com/ipfs/js-ipfs/commit/8bcf56fbec7324dc13d3ec5dce08806a6ef2f974))
* flaky timeout test ([#3767](https://github.com/ipfs/js-ipfs/issues/3767)) ([55afc2f](https://github.com/ipfs/js-ipfs/commit/55afc2f8ee483f4b2807598b7371561d39229e17))
* make "ipfs resolve" cli command recursive by default ([#3707](https://github.com/ipfs/js-ipfs/issues/3707)) ([399ce36](https://github.com/ipfs/js-ipfs/commit/399ce367a1dbc531b52fe228ee4212008c9a1091)), closes [#3692](https://github.com/ipfs/js-ipfs/issues/3692)
### Features
* implement dag import/export ([#3728](https://github.com/ipfs/js-ipfs/issues/3728)) ([700765b](https://github.com/ipfs/js-ipfs/commit/700765be2634fa5d2d71d8b87cf68c9cd328d2c4)), closes [#2953](https://github.com/ipfs/js-ipfs/issues/2953) [#2745](https://github.com/ipfs/js-ipfs/issues/2745)
* upgrade to the new multiformats ([#3556](https://github.com/ipfs/js-ipfs/issues/3556)) ([d13d15f](https://github.com/ipfs/js-ipfs/commit/d13d15f022a87d04a35f0f7822142f9cb898479c))
### BREAKING CHANGES
* resolve is now recursive by default
Co-authored-by: Alex Potsides
* ipld-formats no longer supported, use multiformat BlockCodecs instead
Co-authored-by: Rod Vagg
Co-authored-by: achingbrain
## [0.147.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.146.1...interface-ipfs-core@0.147.0) (2021-06-18)
### Features
* support v2 ipns signatures ([#3708](https://github.com/ipfs/js-ipfs/issues/3708)) ([ade01d1](https://github.com/ipfs/js-ipfs/commit/ade01d138bb185fda902c0a3f7fa14d5bfd48a5e))
## [0.146.1](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.146.0...interface-ipfs-core@0.146.1) (2021-06-05)
### Bug Fixes
* stalling subscription on (node) http-client when daemon is stopped ([#3468](https://github.com/ipfs/js-ipfs/issues/3468)) ([0266abf](https://github.com/ipfs/js-ipfs/commit/0266abf0c4b817636172f78c6e91eb4dd5aad451)), closes [#3465](https://github.com/ipfs/js-ipfs/issues/3465)
## [0.146.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.145.1...interface-ipfs-core@0.146.0) (2021-05-26)
### Features
* allow passing the id of a network peer to ipfs.id ([#3386](https://github.com/ipfs/js-ipfs/issues/3386)) ([00fd709](https://github.com/ipfs/js-ipfs/commit/00fd709a7b71e7cf354ea452ebce460dd7375d34))
## [0.145.1](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.145.0...interface-ipfs-core@0.145.1) (2021-05-11)
### Bug Fixes
* ipfs get with raw blocks ([#3683](https://github.com/ipfs/js-ipfs/issues/3683)) ([28235b0](https://github.com/ipfs/js-ipfs/commit/28235b02558c513e1119dfd3d12b622d67546eca)), closes [#3682](https://github.com/ipfs/js-ipfs/issues/3682)
## [0.145.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.144.2...interface-ipfs-core@0.145.0) (2021-05-10)
### Bug Fixes
* mark ipld options as partial ([#3669](https://github.com/ipfs/js-ipfs/issues/3669)) ([f98af8e](https://github.com/ipfs/js-ipfs/commit/f98af8ed24784929898bb5d33a64dc442c77074d))
* only accept cid for ipfs.dag.get ([#3675](https://github.com/ipfs/js-ipfs/issues/3675)) ([bb8f8bc](https://github.com/ipfs/js-ipfs/commit/bb8f8bc501ffc1ee0f064ba61ec0bca4015bf6ad)), closes [#3637](https://github.com/ipfs/js-ipfs/issues/3637)
### chore
* upgrade deps with new typedefs ([#3550](https://github.com/ipfs/js-ipfs/issues/3550)) ([a418a52](https://github.com/ipfs/js-ipfs/commit/a418a521574c878d7aabd0ad2fd8d516908a3756))
### Features
* support identity hash in block.get + dag.get ([#3616](https://github.com/ipfs/js-ipfs/issues/3616)) ([28ad9ad](https://github.com/ipfs/js-ipfs/commit/28ad9ad6e50abb89a366ecd6b5301e848f0e9962))
### BREAKING CHANGES
* all core api methods now have types, some method signatures have changed, named exports are now used by the http, grpc and ipfs client modules
## [0.144.2](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.144.1...interface-ipfs-core@0.144.2) (2021-03-09)
### Bug Fixes
* update to new aegir ([#3528](https://github.com/ipfs/js-ipfs/issues/3528)) ([49f7880](https://github.com/ipfs/js-ipfs/commit/49f78807d7e26483bd926b45cc7e0f797d77e41b))
## [0.144.1](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.144.0...interface-ipfs-core@0.144.1) (2021-02-08)
**Note:** Version bump only for package interface-ipfs-core
## [0.144.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.143.1...interface-ipfs-core@0.144.0) (2021-02-01)
### Bug Fixes
* updates webpack example to use v5 ([#3512](https://github.com/ipfs/js-ipfs/issues/3512)) ([c7110db](https://github.com/ipfs/js-ipfs/commit/c7110db71b5c0f0f9f415f31f91b5b228341e13e)), closes [#3511](https://github.com/ipfs/js-ipfs/issues/3511)
### chore
* update deps ([#3514](https://github.com/ipfs/js-ipfs/issues/3514)) ([061d77c](https://github.com/ipfs/js-ipfs/commit/061d77cc03f40af5a3bc3590481e1e5836e7f0d8))
### Features
* support remote pinning services in ipfs-http-client ([#3293](https://github.com/ipfs/js-ipfs/issues/3293)) ([ba240fd](https://github.com/ipfs/js-ipfs/commit/ba240fdf93edc88028315483240d7822a7ca88ed))
### BREAKING CHANGES
* ipfs-repo upgrade requires repo migration to v10
## [0.143.1](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.143.0...interface-ipfs-core@0.143.1) (2021-01-20)
**Note:** Version bump only for package interface-ipfs-core
## [0.143.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.142.3...interface-ipfs-core@0.143.0) (2021-01-15)
### chore
* update libp2p to 0.30 ([#3427](https://github.com/ipfs/js-ipfs/issues/3427)) ([a39e6fb](https://github.com/ipfs/js-ipfs/commit/a39e6fb372bf9e7782462b6a4b7530a3f8c9b3f1))
### Features
* add grpc server and client ([#3403](https://github.com/ipfs/js-ipfs/issues/3403)) ([a9027e0](https://github.com/ipfs/js-ipfs/commit/a9027e0ec0cea9a4f34b4f2f52e09abb35237384)), closes [#2519](https://github.com/ipfs/js-ipfs/issues/2519) [#2838](https://github.com/ipfs/js-ipfs/issues/2838) [#2943](https://github.com/ipfs/js-ipfs/issues/2943) [#2854](https://github.com/ipfs/js-ipfs/issues/2854) [#2864](https://github.com/ipfs/js-ipfs/issues/2864)
* allow passing a http.Agent to the grpc client ([#3477](https://github.com/ipfs/js-ipfs/issues/3477)) ([c5f0bc5](https://github.com/ipfs/js-ipfs/commit/c5f0bc5eeee15369b7d02901035b04184a8608d2)), closes [#3474](https://github.com/ipfs/js-ipfs/issues/3474)
### BREAKING CHANGES
* The websocket transport will only dial DNS+WSS addresses - see https://github.com/libp2p/js-libp2p-websockets/releases/tag/v0.15.0
Co-authored-by: Hugo Dias
## [0.142.3](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.142.2...interface-ipfs-core@0.142.3) (2020-12-16)
### Bug Fixes
* fix ipfs.ls() for a single file object ([#3440](https://github.com/ipfs/js-ipfs/issues/3440)) ([f243dd1](https://github.com/ipfs/js-ipfs/commit/f243dd1c37fcb9786d77d129cd9b238457d18a15))
* regressions introduced by new releases of CID & multicodec ([#3442](https://github.com/ipfs/js-ipfs/issues/3442)) ([b5152d8](https://github.com/ipfs/js-ipfs/commit/b5152d8cc93ecc8d39fc353ea66d7eaf1661e3c0)), closes [/github.com/multiformats/js-cid/commit/0e11f035c9230e7f6d79c159ace9b80de88cb5eb#diff-25a6634263c1b1f6fc4697a04e2b9904ea4b042a89af59dc93ec1f5d44848a26](https://github.com//github.com/multiformats/js-cid/commit/0e11f035c9230e7f6d79c159ace9b80de88cb5eb/issues/diff-25a6634263c1b1f6fc4697a04e2b9904ea4b042a89af59dc93ec1f5d44848a26)
## [0.142.2](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.142.1...interface-ipfs-core@0.142.2) (2020-11-25)
**Note:** Version bump only for package interface-ipfs-core
## [0.142.1](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.142.0...interface-ipfs-core@0.142.1) (2020-11-16)
### Bug Fixes
* align behaviour between go and js for content without paths ([#3385](https://github.com/ipfs/js-ipfs/issues/3385)) ([334873d](https://github.com/ipfs/js-ipfs/commit/334873d3784e2baa2b19f8f69b5aade36715ba03))
* ensure correct progress is reported ([#3384](https://github.com/ipfs/js-ipfs/issues/3384)) ([633d870](https://github.com/ipfs/js-ipfs/commit/633d8704f74534542f54536bc6960528214339a2))
* report ipfs.add progress over http ([#3310](https://github.com/ipfs/js-ipfs/issues/3310)) ([39cad4b](https://github.com/ipfs/js-ipfs/commit/39cad4b76b950ea6a76477fd01f8631b8bd9aa1e))
## [0.142.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.141.0...interface-ipfs-core@0.142.0) (2020-11-09)
### Features
* pass file name to add/addAll progress handler ([#3372](https://github.com/ipfs/js-ipfs/issues/3372)) ([69681a7](https://github.com/ipfs/js-ipfs/commit/69681a7d7a8434c11f6f10e370e324f5a3d31042)), closes [ipfs/js-ipfs-unixfs#87](https://github.com/ipfs/js-ipfs-unixfs/issues/87)
## [0.141.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.140.0...interface-ipfs-core@0.141.0) (2020-10-28)
### Bug Fixes
* files ls should return string ([#3352](https://github.com/ipfs/js-ipfs/issues/3352)) ([16ecc74](https://github.com/ipfs/js-ipfs/commit/16ecc7485dfbb1f0c827c5f804974bb804f3dafd)), closes [#3345](https://github.com/ipfs/js-ipfs/issues/3345) [#2939](https://github.com/ipfs/js-ipfs/issues/2939) [#3330](https://github.com/ipfs/js-ipfs/issues/3330) [#2948](https://github.com/ipfs/js-ipfs/issues/2948)
* use fetch in electron renderer and electron-fetch in main ([#3251](https://github.com/ipfs/js-ipfs/issues/3251)) ([639d71f](https://github.com/ipfs/js-ipfs/commit/639d71f7ac8f66d9633e753a2a6be927e14a5af0))
### Features
* type check & generate defs from jsdoc ([#3281](https://github.com/ipfs/js-ipfs/issues/3281)) ([bbcaf34](https://github.com/ipfs/js-ipfs/commit/bbcaf34111251b142273a5675f4754ff68bd9fa0))
### BREAKING CHANGES
* types returned by `ipfs.files.ls` are now strings, in line with the docs but different to previous behaviour
Co-authored-by: Geoffrey Cohler
## [0.140.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.139.1...interface-ipfs-core@0.140.0) (2020-09-03)
### Bug Fixes
* handle progress for empty files ([#3260](https://github.com/ipfs/js-ipfs/issues/3260)) ([9c36cb8](https://github.com/ipfs/js-ipfs/commit/9c36cb8f0c122e78c3cda3d0769d66c4d380787a)), closes [#3255](https://github.com/ipfs/js-ipfs/issues/3255)
### Features
* add protocol list to ipfs id ([#3250](https://github.com/ipfs/js-ipfs/issues/3250)) ([1b6cf60](https://github.com/ipfs/js-ipfs/commit/1b6cf600a6b1348199457ca1fe6f314b6eff8c46))
* ipns publish example ([#3207](https://github.com/ipfs/js-ipfs/issues/3207)) ([91faec6](https://github.com/ipfs/js-ipfs/commit/91faec6e3d89b0d9883b8d7815c276d44048e739))
* store pins in datastore instead of a DAG ([#2771](https://github.com/ipfs/js-ipfs/issues/2771)) ([64b7fe4](https://github.com/ipfs/js-ipfs/commit/64b7fe41738cbe96d5a9075f0c01156c6f889c40))
* update hapi to v20 ([#3245](https://github.com/ipfs/js-ipfs/issues/3245)) ([1aeef89](https://github.com/ipfs/js-ipfs/commit/1aeef89c73f42a2f6cceb7f0598400141ce40e23))
## [0.139.1](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.139.0...interface-ipfs-core@0.139.1) (2020-08-24)
### Bug Fixes
* validate ipns records with inline public keys ([#3224](https://github.com/ipfs/js-ipfs/issues/3224)) ([5cc0e08](https://github.com/ipfs/js-ipfs/commit/5cc0e086b036e7ba40b09768b67b7067adca43c1))
## [0.139.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.138.0...interface-ipfs-core@0.139.0) (2020-08-12)
### Bug Fixes
* support keychain without pass ([#3212](https://github.com/ipfs/js-ipfs/issues/3212)) ([7e0e85c](https://github.com/ipfs/js-ipfs/commit/7e0e85c2f003a09845b1dbe4200ca61366933b05))
### Features
* share IPFS node between browser tabs ([#3081](https://github.com/ipfs/js-ipfs/issues/3081)) ([1b8b1b8](https://github.com/ipfs/js-ipfs/commit/1b8b1b822a252498889c54972a1f57e1fedc39d0)), closes [#3022](https://github.com/ipfs/js-ipfs/issues/3022)
### BREAKING CHANGES
* remove support for key.export over the http api
## [0.138.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.137.0...interface-ipfs-core@0.138.0) (2020-07-16)
### Bug Fixes
* optional arguments go in the options object ([#3118](https://github.com/ipfs/js-ipfs/issues/3118)) ([8cb8c73](https://github.com/ipfs/js-ipfs/commit/8cb8c73037e44894d756b70f344b3282463206f9))
### Features
* add interface and http client versions to version output ([#3125](https://github.com/ipfs/js-ipfs/issues/3125)) ([65f8b23](https://github.com/ipfs/js-ipfs/commit/65f8b23f550f939e94aaf6939894a513519e6d68)), closes [#2878](https://github.com/ipfs/js-ipfs/issues/2878)
* store blocks by multihash instead of CID ([#3124](https://github.com/ipfs/js-ipfs/issues/3124)) ([03b17f5](https://github.com/ipfs/js-ipfs/commit/03b17f5e2d290e84aa0cb541079b79e468e7d1bd))
## [0.137.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.136.0...interface-ipfs-core@0.137.0) (2020-06-24)
### Features
* add config.getAll ([#3071](https://github.com/ipfs/js-ipfs/issues/3071)) ([16587f1](https://github.com/ipfs/js-ipfs/commit/16587f16e1b3ae525c099b1975748510638aceee))
## [0.136.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.135.1...interface-ipfs-core@0.136.0) (2020-06-05)
### Features
* sync with go-ipfs 0.5 ([#3013](https://github.com/ipfs/js-ipfs/issues/3013)) ([0900bb9](https://github.com/ipfs/js-ipfs/commit/0900bb9b8123edb689a137a006c5507d8503f693))
## [0.135.1](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.135.0...interface-ipfs-core@0.135.1) (2020-05-29)
**Note:** Version bump only for package interface-ipfs-core
## [0.135.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.134.3...interface-ipfs-core@0.135.0) (2020-05-18)
### Bug Fixes
* fixes browser script tag example ([#3034](https://github.com/ipfs/js-ipfs/issues/3034)) ([ee8b769](https://github.com/ipfs/js-ipfs/commit/ee8b769b96f7e3c8414bbf85853ab4e21e8fd11c)), closes [#3027](https://github.com/ipfs/js-ipfs/issues/3027)
* remove node globals ([#2932](https://github.com/ipfs/js-ipfs/issues/2932)) ([d0d2f74](https://github.com/ipfs/js-ipfs/commit/d0d2f74cef4e439c6d2baadba1f1f9f52534fcba))
* typeof bug when passing timeout to dag.get ([#3035](https://github.com/ipfs/js-ipfs/issues/3035)) ([026a542](https://github.com/ipfs/js-ipfs/commit/026a5423e00992968840c9236afe47bdab9ee834))
### Features
* cancellable api calls ([#2993](https://github.com/ipfs/js-ipfs/issues/2993)) ([2b24f59](https://github.com/ipfs/js-ipfs/commit/2b24f590041a0df9da87b75ae2344232fe22fe3a)), closes [#3015](https://github.com/ipfs/js-ipfs/issues/3015)
## [0.134.3](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.134.2...interface-ipfs-core@0.134.3) (2020-05-05)
**Note:** Version bump only for package interface-ipfs-core
## [0.134.2](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.134.1...interface-ipfs-core@0.134.2) (2020-05-05)
### Bug Fixes
* pass headers to request ([#3018](https://github.com/ipfs/js-ipfs/issues/3018)) ([3ba00f8](https://github.com/ipfs/js-ipfs/commit/3ba00f8c6a8a057c5776d539a671a74d9565fb29)), closes [#3017](https://github.com/ipfs/js-ipfs/issues/3017)
## [0.134.1](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.134.0...interface-ipfs-core@0.134.1) (2020-04-28)
### Bug Fixes
* fix gc tests ([#3008](https://github.com/ipfs/js-ipfs/issues/3008)) ([9f7f03e](https://github.com/ipfs/js-ipfs/commit/9f7f03e1ea672834b7f984657c7d7d7c768bcd6c))
## [0.134.0](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.133.1...interface-ipfs-core@0.134.0) (2020-04-16)
### Bug Fixes
* make http api only accept POST requests ([#2977](https://github.com/ipfs/js-ipfs/issues/2977)) ([943d4a8](https://github.com/ipfs/js-ipfs/commit/943d4a8cf2d4c4ff5ecd4814c59cb0aae0cfa1fd))
* pass timeout arg to server ([#2979](https://github.com/ipfs/js-ipfs/issues/2979)) ([049f085](https://github.com/ipfs/js-ipfs/commit/049f085fd206a1afb729fa825d8df38bf7aa8549))
### BREAKING CHANGES
* Where we used to accept all and any HTTP methods, now only POST is
accepted. The API client will now only send POST requests too.
* test: add tests to make sure we are post-only
* chore: upgrade ipfs-utils
* fix: return 405 instead of 404 for bad methods
* fix: reject browsers that do not send an origin
Also fixes running interface tests over http in browsers against
js-ipfs
## [0.133.1](https://github.com/ipfs/js-ipfs/compare/interface-ipfs-core@0.133.0...interface-ipfs-core@0.133.1) (2020-04-08)
**Note:** Version bump only for package interface-ipfs-core
# 0.133.0 (2020-03-31)
### Bug Fixes
* avoid throw error when use readme code ([#2934](https://github.com/ipfs/js-ipfs/issues/2934)) ([b18f6e1](https://github.com/ipfs/js-ipfs/commit/b18f6e1a791f9c72c9f35ec78c471879bbdc1525))
* dont include util.textencoder in the browser ([#2919](https://github.com/ipfs/js-ipfs/issues/2919)) ([3207e3b](https://github.com/ipfs/js-ipfs/commit/3207e3b35c9c250332c03dd2a066e8ebcda35e43))
### chore
* move mfs and multipart files into core ([#2811](https://github.com/ipfs/js-ipfs/issues/2811)) ([82b9e08](https://github.com/ipfs/js-ipfs/commit/82b9e085330e6c6290e6f3dd29678247984ffdce))
* update dep version and ignore interop test for raw leaves ([#2747](https://github.com/ipfs/js-ipfs/issues/2747)) ([6376cec](https://github.com/ipfs/js-ipfs/commit/6376cec2b4beccef4751c498088f600ec7788118))
### Features
* remove ky from http-client and utils ([#2810](https://github.com/ipfs/js-ipfs/issues/2810)) ([9bc9625](https://github.com/ipfs/js-ipfs/commit/9bc96252686d0bbbfdb2a3300bb17b80eafdaf00)), closes [#2801](https://github.com/ipfs/js-ipfs/issues/2801)
### BREAKING CHANGES
* When the path passed to `ipfs.files.stat(path)` was a hamt sharded dir, the resovled
value returned by js-ipfs previously had a `type` property of with a value of
`'hamt-sharded-directory'`. To bring it in line with go-ipfs this value is now
`'directory'`.
* Files that fit into one block imported with either `--cid-version=1`
or `--raw-leaves=true` previously returned a CID that resolved to
a raw node (e.g. a buffer). Returned CIDs now resolve to a `dag-pb`
node that contains a UnixFS entry. This is to allow setting metadata
on small files with CIDv1.
## [0.132.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.131.7...v0.132.0) (2020-02-09)
### Bug Fixes
* add object.stat timeout leeway ([#586](https://github.com/ipfs/interface-ipfs-core/issues/586)) ([8b45ad0](https://github.com/ipfs/interface-ipfs-core/commit/8b45ad0))
## [0.131.7](https://github.com/ipfs/interface-ipfs-core/compare/v0.131.6...v0.131.7) (2020-02-03)
### Bug Fixes
* only expect no multiaddrs if node is in-proc webworker ([4e25b4f](https://github.com/ipfs/interface-ipfs-core/commit/4e25b4f))
## [0.131.6](https://github.com/ipfs/interface-ipfs-core/compare/v0.131.5...v0.131.6) (2020-02-03)
### Bug Fixes
* use go for webworker tests ([3a96093](https://github.com/ipfs/interface-ipfs-core/commit/3a96093))
## [0.131.5](https://github.com/ipfs/interface-ipfs-core/compare/v0.131.4...v0.131.5) (2020-02-03)
### Bug Fixes
* do not spawn go nodes with webrtc swarm addresses ([c633d08](https://github.com/ipfs/interface-ipfs-core/commit/c633d08))
## [0.131.4](https://github.com/ipfs/interface-ipfs-core/compare/v0.131.3...v0.131.4) (2020-02-02)
### Bug Fixes
* use js for pubsub tests as before ([ade2145](https://github.com/ipfs/interface-ipfs-core/commit/ade2145))
## [0.131.3](https://github.com/ipfs/interface-ipfs-core/compare/v0.131.2...v0.131.3) (2020-02-02)
### Bug Fixes
* spawn dialable nodes when testing with webworkers ([df7cb3a](https://github.com/ipfs/interface-ipfs-core/commit/df7cb3a))
## [0.131.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.131.1...v0.131.2) (2020-02-01)
### Bug Fixes
* fix swarm peer tests for electron ([ac7cedf](https://github.com/ipfs/interface-ipfs-core/commit/ac7cedf))
## [0.131.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.131.0...v0.131.1) (2020-01-31)
### Bug Fixes
* fix up peer test ([0b80a20](https://github.com/ipfs/interface-ipfs-core/commit/0b80a20))
## [0.131.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.130.0...v0.131.0) (2020-01-31)
### Bug Fixes
* do not assume certain implementations of ipfs are present ([#584](https://github.com/ipfs/interface-ipfs-core/issues/584)) ([3d24911](https://github.com/ipfs/interface-ipfs-core/commit/3d24911))
## [0.130.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.129.0...v0.130.0) (2020-01-29)
### Code Refactoring
* return peer ids as strings ([#581](https://github.com/ipfs/interface-ipfs-core/issues/581)) ([153fd24](https://github.com/ipfs/interface-ipfs-core/commit/153fd24))
### BREAKING CHANGES
* Where `PeerID`s were previously [CID]s, now they are Strings
- `ipfs.bitswap.stat().peers[n]` is now a String (was a CID)
- `ipfs.dht.findPeer().id` is now a String (was a CID)
- `ipfs.dht.findProvs()[n].id` is now a String (was a CID)
- `ipfs.dht.provide()[n].id` is now a String (was a CID)
- `ipfs.dht.put()[n].id` is now a String (was a CID)
- `ipfs.dht.query()[n].id` is now a String (was a CID)
- `ipfs.id().id` is now a String (was a CID)
- `ipfs.id().addresses[n]` are now [Multiaddr]s (were Strings)
[CID]: https://www.npmjs.com/package/cids
[Multiaddr]: https://www.npmjs.com/package/multiaddr
## [0.129.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.128.0...v0.129.0) (2020-01-23)
## [0.128.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.127.0...v0.128.0) (2020-01-22)
## [0.127.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.126.0...v0.127.0) (2020-01-11)
## [0.126.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.125.0...v0.126.0) (2020-01-09)
## [0.125.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.124.1...v0.125.0) (2019-12-11)
### Bug Fixes
* handle err on both start and stop echo-server ([#569](https://github.com/ipfs/interface-ipfs-core/issues/569)) ([d25c6f6](https://github.com/ipfs/interface-ipfs-core/commit/d25c6f6))
### Features
* add support for new ipfsd-ctl ([#541](https://github.com/ipfs/interface-ipfs-core/issues/541)) ([a27cfa7](https://github.com/ipfs/interface-ipfs-core/commit/a27cfa7))
## [0.124.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.124.0...v0.124.1) (2019-12-10)
## [0.124.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.123.0...v0.124.0) (2019-12-02)
## [0.123.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.122.0...v0.123.0) (2019-11-27)
## [0.122.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.121.0...v0.122.0) (2019-11-26)
## [0.121.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.120.0...v0.121.0) (2019-11-19)
### Bug Fixes
* allow offline option casing ([#561](https://github.com/ipfs/interface-ipfs-core/issues/561)) ([f08b0fd](https://github.com/ipfs/interface-ipfs-core/commit/f08b0fd))
## [0.120.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.119.0...v0.120.0) (2019-11-19)
### Bug Fixes
* parents option and ls stream flow ([#558](https://github.com/ipfs/interface-ipfs-core/issues/558)) ([b9df5fb](https://github.com/ipfs/interface-ipfs-core/commit/b9df5fb))
## [0.119.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.118.0...v0.119.0) (2019-11-11)
## [0.118.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.117.2...v0.118.0) (2019-11-06)
### Features
* test ipns resolve of peerid as cid ([#553](https://github.com/ipfs/interface-ipfs-core/issues/553)) ([9193957](https://github.com/ipfs/interface-ipfs-core/commit/9193957))
## [0.117.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.117.1...v0.117.2) (2019-10-05)
## [0.117.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.117.0...v0.117.1) (2019-10-05)
## [0.117.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.116.0...v0.117.0) (2019-10-04)
### Documentation
* add dry-run config test and change new/old for original/updated ([e206aa7](https://github.com/ipfs/interface-ipfs-core/commit/e206aa7))
### BREAKING CHANGES
* `ipfs.config.profiles.apply` now returns `original`/`updated` keys
in the diff because using `new` stops us from destructuring in js.
## [0.116.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.115.3...v0.116.0) (2019-10-04)
### Features
* add test for listing config profiles ([142a373](https://github.com/ipfs/interface-ipfs-core/commit/142a373))
## [0.115.3](https://github.com/ipfs/interface-ipfs-core/compare/v0.115.2...v0.115.3) (2019-10-04)
## [0.115.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.115.1...v0.115.2) (2019-10-04)
### Bug Fixes
* configure chai for use by other modules ([77c8be9](https://github.com/ipfs/interface-ipfs-core/commit/77c8be9))
* make invalid url actually invalid ([30a84fb](https://github.com/ipfs/interface-ipfs-core/commit/30a84fb))
* test setting boolean configs keys on boolean fields ([d937fc1](https://github.com/ipfs/interface-ipfs-core/commit/d937fc1))
## [0.115.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.115.0...v0.115.1) (2019-10-01)
## [0.115.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.114.0...v0.115.0) (2019-09-25)
## [0.114.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.113.1...v0.114.0) (2019-09-16)
### Bug Fixes
* change swarm test ([00341f9](https://github.com/ipfs/interface-ipfs-core/commit/00341f9))
* new setup ([b724e65](https://github.com/ipfs/interface-ipfs-core/commit/b724e65))
## [0.113.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.113.0...v0.113.1) (2019-09-13)
### Bug Fixes
* make pubsub unsubscribe tests work in electron renderer ([eedfe3d](https://github.com/ipfs/interface-ipfs-core/commit/eedfe3d))
## [0.113.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.112.0...v0.113.0) (2019-09-05)
### Bug Fixes
* **package:** update ipfs-utils to version 0.1.0 ([#521](https://github.com/ipfs/interface-ipfs-core/issues/521)) ([56caa89](https://github.com/ipfs/interface-ipfs-core/commit/56caa89))
## [0.112.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.111.1...v0.112.0) (2019-09-03)
### Bug Fixes
* supported add inputs ([#519](https://github.com/ipfs/interface-ipfs-core/issues/519)) ([ddc4fe7](https://github.com/ipfs/interface-ipfs-core/commit/ddc4fe7))
## [0.111.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.111.0...v0.111.1) (2019-08-30)
### Bug Fixes
* change `cp` and `mv` tests to the current spec ([#515](https://github.com/ipfs/interface-ipfs-core/issues/515)) ([b107e57](https://github.com/ipfs/interface-ipfs-core/commit/b107e57))
## [0.111.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.110.0...v0.111.0) (2019-08-28)
## [0.110.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.109.1...v0.110.0) (2019-08-27)
### Bug Fixes
* reduce the number of concurrent requests in browser ([#505](https://github.com/ipfs/interface-ipfs-core/issues/505)) ([7596634](https://github.com/ipfs/interface-ipfs-core/commit/7596634))
## [0.109.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.109.0...v0.109.1) (2019-08-06)
## [0.109.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.108.1...v0.109.0) (2019-07-26)
### Bug Fixes
* resolve IPNS recursively test ([#507](https://github.com/ipfs/interface-ipfs-core/issues/507)) ([1db8abe](https://github.com/ipfs/interface-ipfs-core/commit/1db8abe))
## [0.108.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.108.0...v0.108.1) (2019-07-25)
### Bug Fixes
* reword resolve test with async/await ([#504](https://github.com/ipfs/interface-ipfs-core/issues/504)) ([3f7410a](https://github.com/ipfs/interface-ipfs-core/commit/3f7410a))
* use the correct option name for files.ls long ([#502](https://github.com/ipfs/interface-ipfs-core/issues/502)) ([ed4988d](https://github.com/ipfs/interface-ipfs-core/commit/ed4988d))
## [0.108.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.107.3...v0.108.0) (2019-07-17)
### Features
* tests for config profile endpoint ([#488](https://github.com/ipfs/interface-ipfs-core/issues/488)) ([e45f39c](https://github.com/ipfs/interface-ipfs-core/commit/e45f39c))
## [0.107.3](https://github.com/ipfs/interface-ipfs-core/compare/v0.107.2...v0.107.3) (2019-07-16)
## [0.107.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.107.1...v0.107.2) (2019-07-16)
### Bug Fixes
* pin.ls ignored opts when hash was present ([#375](https://github.com/ipfs/interface-ipfs-core/issues/375)) ([be72ed6](https://github.com/ipfs/interface-ipfs-core/commit/be72ed6))
## [0.107.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.107.0...v0.107.1) (2019-07-11)
## [0.107.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.106.0...v0.107.0) (2019-07-11)
### Bug Fixes
* repo.gc() response format ([#492](https://github.com/ipfs/interface-ipfs-core/issues/492)) ([a2ec3f6](https://github.com/ipfs/interface-ipfs-core/commit/a2ec3f6))
## [0.106.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.105.1...v0.106.0) (2019-07-05)
## [0.105.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.105.0...v0.105.1) (2019-07-03)
### Bug Fixes
* wait for one key to be the required key not all ([#490](https://github.com/ipfs/interface-ipfs-core/issues/490)) ([acea55f](https://github.com/ipfs/interface-ipfs-core/commit/acea55f))
## [0.105.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.104.2...v0.105.0) (2019-06-20)
### Features
* add support to resolve dns to ipns ([#458](https://github.com/ipfs/interface-ipfs-core/issues/458)) ([cd41a3c](https://github.com/ipfs/interface-ipfs-core/commit/cd41a3c))
## [0.104.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.104.1...v0.104.2) (2019-05-31)
## [0.104.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.104.0...v0.104.1) (2019-05-31)
### Bug Fixes
* dht tests ([#486](https://github.com/ipfs/interface-ipfs-core/issues/486)) ([2952672](https://github.com/ipfs/interface-ipfs-core/commit/2952672))
* use cidVersion option ([#484](https://github.com/ipfs/interface-ipfs-core/issues/484)) ([e00eb4a](https://github.com/ipfs/interface-ipfs-core/commit/e00eb4a))
* **package:** update async to version 3.0.1 ([#481](https://github.com/ipfs/interface-ipfs-core/issues/481)) ([b60fe33](https://github.com/ipfs/interface-ipfs-core/commit/b60fe33))
## [0.104.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.103.0...v0.104.0) (2019-05-24)
## [0.103.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.102.0...v0.103.0) (2019-05-21)
## [0.102.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.101.1...v0.102.0) (2019-05-16)
### Features
* add tests for add data using File DOM api ([#461](https://github.com/ipfs/interface-ipfs-core/issues/461)) ([86a1f3f](https://github.com/ipfs/interface-ipfs-core/commit/86a1f3f))
## [0.101.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.101.0...v0.101.1) (2019-05-16)
### Bug Fixes
* use fixtures for refs tests ([#471](https://github.com/ipfs/interface-ipfs-core/issues/471)) ([3f30830](https://github.com/ipfs/interface-ipfs-core/commit/3f30830))
## [0.101.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.100.1...v0.101.0) (2019-05-15)
## [0.100.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.100.0...v0.100.1) (2019-05-13)
### Reverts
* add test for Object.links with CBOR ([#465](https://github.com/ipfs/interface-ipfs-core/issues/465)) ([4c3d84d](https://github.com/ipfs/interface-ipfs-core/commit/4c3d84d))
## [0.100.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.99.2...v0.100.0) (2019-05-08)
## [0.99.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.99.1...v0.99.2) (2019-04-08)
## [0.99.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.99.0...v0.99.1) (2019-04-04)
### Bug Fixes
* swarm addrs test ([#454](https://github.com/ipfs/interface-ipfs-core/issues/454)) ([16ad830](https://github.com/ipfs/interface-ipfs-core/commit/16ad830))
## [0.99.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.98.1...v0.99.0) (2019-03-13)
### Bug Fixes
* don't expect ipfs to preserve a leading slash ([#440](https://github.com/ipfs/interface-ipfs-core/issues/440)) ([d3ad40b](https://github.com/ipfs/interface-ipfs-core/commit/d3ad40b))
* ls files sizes for compat with go-ipfs 0.4.19 ([#449](https://github.com/ipfs/interface-ipfs-core/issues/449)) ([2ef1480](https://github.com/ipfs/interface-ipfs-core/commit/2ef1480)), closes [#427](https://github.com/ipfs/interface-ipfs-core/issues/427)
## [0.98.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.98.0...v0.98.1) (2019-03-13)
## [0.98.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.97.1...v0.98.0) (2019-02-26)
## [0.97.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.97.0...v0.97.1) (2019-02-19)
### Bug Fixes
* populate in series ([#443](https://github.com/ipfs/interface-ipfs-core/issues/443)) ([06a3980](https://github.com/ipfs/interface-ipfs-core/commit/06a3980))
## [0.97.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.96.1...v0.97.0) (2019-02-19)
### Bug Fixes
* add new SSL certificate ([#432](https://github.com/ipfs/interface-ipfs-core/issues/432)) ([fe539e6](https://github.com/ipfs/interface-ipfs-core/commit/fe539e6))
* add test for dag get with localResolve option ([#433](https://github.com/ipfs/interface-ipfs-core/issues/433)) ([44d4803](https://github.com/ipfs/interface-ipfs-core/commit/44d4803))
## [0.96.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.96.0...v0.96.1) (2019-01-15)
## [0.96.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.95.0...v0.96.0) (2019-01-14)
## [0.95.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.94.0...v0.95.0) (2019-01-04)
## [0.94.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.93.0...v0.94.0) (2018-12-16)
## [0.93.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.92.0...v0.93.0) (2018-12-14)
### Bug Fixes
* allow only by object ([#407](https://github.com/ipfs/interface-ipfs-core/issues/407)) ([1766ef4](https://github.com/ipfs/interface-ipfs-core/commit/1766ef4))
* dht find peer ([#418](https://github.com/ipfs/interface-ipfs-core/issues/418)) ([8b890b6](https://github.com/ipfs/interface-ipfs-core/commit/8b890b6))
## [0.92.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.91.1...v0.92.0) (2018-12-12)
### Bug Fixes
* addFromURL case ([#415](https://github.com/ipfs/interface-ipfs-core/issues/415)) ([f54422d](https://github.com/ipfs/interface-ipfs-core/commit/f54422d))
## [0.91.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.91.0...v0.91.1) (2018-12-11)
### Bug Fixes
* change find provs options test ([#416](https://github.com/ipfs/interface-ipfs-core/issues/416)) ([3c08aa2](https://github.com/ipfs/interface-ipfs-core/commit/3c08aa2))
## [0.91.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.90.0...v0.91.0) (2018-12-10)
### Bug Fixes
* another typo ([87bcd68](https://github.com/ipfs/interface-ipfs-core/commit/87bcd68))
* typos ([e7b8697](https://github.com/ipfs/interface-ipfs-core/commit/e7b8697))
* update dht responses ([#389](https://github.com/ipfs/interface-ipfs-core/issues/389)) ([c4bea6f](https://github.com/ipfs/interface-ipfs-core/commit/c4bea6f))
* Updated link in README ([#411](https://github.com/ipfs/interface-ipfs-core/issues/411)) ([81a5798](https://github.com/ipfs/interface-ipfs-core/commit/81a5798))
## [0.90.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.89.0...v0.90.0) (2018-12-05)
## [0.89.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.88.0...v0.89.0) (2018-12-03)
### Bug Fixes
* code blocks for the code ([36cf442](https://github.com/ipfs/interface-ipfs-core/commit/36cf442))
* ipns over pubsub tests ([#395](https://github.com/ipfs/interface-ipfs-core/issues/395)) ([e872b8a](https://github.com/ipfs/interface-ipfs-core/commit/e872b8a))
## [0.88.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.87.0...v0.88.0) (2018-11-27)
## [0.87.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.86.0...v0.87.0) (2018-11-26)
## [0.86.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.85.0...v0.86.0) (2018-11-12)
### Features
* move regular files api to top level, add addFromFs and addFromURL ([#378](https://github.com/ipfs/interface-ipfs-core/issues/378)) ([3dc7278](https://github.com/ipfs/interface-ipfs-core/commit/3dc7278))
## [0.85.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.84.3...v0.85.0) (2018-11-12)
### Bug Fixes
* updates ipld-dag-pb dep to version without .cid properties ([#388](https://github.com/ipfs/interface-ipfs-core/issues/388)) ([b8f7b9a](https://github.com/ipfs/interface-ipfs-core/commit/b8f7b9a))
## [0.84.3](https://github.com/ipfs/interface-ipfs-core/compare/v0.84.2...v0.84.3) (2018-10-31)
### Bug Fixes
* we cant rely on error messages yet, not standardized ([fdb4998](https://github.com/ipfs/interface-ipfs-core/commit/fdb4998))
## [0.84.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.84.1...v0.84.2) (2018-10-31)
## [0.84.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.84.0...v0.84.1) (2018-10-31)
## [0.84.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.83.0...v0.84.0) (2018-10-31)
### Bug Fixes
* ping tests ([cd00d5d](https://github.com/ipfs/interface-ipfs-core/commit/cd00d5d))
* remove antipattern from ping tests ([2e822b6](https://github.com/ipfs/interface-ipfs-core/commit/2e822b6))
## [0.83.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.82.0...v0.83.0) (2018-10-30)
## [0.82.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.81.0...v0.82.0) (2018-10-30)
## [0.81.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.80.0...v0.81.0) (2018-10-29)
## [0.80.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.79.0...v0.80.0) (2018-10-18)
## [0.79.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.78.0...v0.79.0) (2018-10-15)
### Bug Fixes
* dht find peer and providers ([#368](https://github.com/ipfs/interface-ipfs-core/issues/368)) ([40f796f](https://github.com/ipfs/interface-ipfs-core/commit/40f796f))
## [0.78.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.77.1...v0.78.0) (2018-09-20)
### Bug Fixes
* example links in miscellaneous spec section ([#364](https://github.com/ipfs/interface-ipfs-core/issues/364)) ([45e8142](https://github.com/ipfs/interface-ipfs-core/commit/45e8142))
* test for buffer with options ([#370](https://github.com/ipfs/interface-ipfs-core/issues/370)) ([d456245](https://github.com/ipfs/interface-ipfs-core/commit/d456245))
## [0.77.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.77.0...v0.77.1) (2018-09-05)
### Bug Fixes
* bitswap.stat docs ([#355](https://github.com/ipfs/interface-ipfs-core/issues/355)) ([f146e1b](https://github.com/ipfs/interface-ipfs-core/commit/f146e1b))
* block CID links ([#356](https://github.com/ipfs/interface-ipfs-core/issues/356)) ([9c4d6e1](https://github.com/ipfs/interface-ipfs-core/commit/9c4d6e1))
* block stat return value key ([1e02740](https://github.com/ipfs/interface-ipfs-core/commit/1e02740))
* ipfs.io now should be resolved recursively ([#362](https://github.com/ipfs/interface-ipfs-core/issues/362)) ([d80d3a3](https://github.com/ipfs/interface-ipfs-core/commit/d80d3a3))
## [0.77.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.76.1...v0.77.0) (2018-08-28)
### Bug Fixes
* remove bitswap.unwant ([#353](https://github.com/ipfs/interface-ipfs-core/issues/353)) ([6065f63](https://github.com/ipfs/interface-ipfs-core/commit/6065f63)), closes [#339](https://github.com/ipfs/interface-ipfs-core/issues/339)
## [0.76.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.76.0...v0.76.1) (2018-08-16)
### Bug Fixes
* allow retries for DNS test due to dependence on external services ([#352](https://github.com/ipfs/interface-ipfs-core/issues/352)) ([5b3f5a8](https://github.com/ipfs/interface-ipfs-core/commit/5b3f5a8))
* typo ([b9dc12a](https://github.com/ipfs/interface-ipfs-core/commit/b9dc12a))
* typo ([2fbf551](https://github.com/ipfs/interface-ipfs-core/commit/2fbf551))
## [0.76.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.75.2...v0.76.0) (2018-08-10)
### Features
* ipns working locally ([#327](https://github.com/ipfs/interface-ipfs-core/issues/327)) ([49a4827](https://github.com/ipfs/interface-ipfs-core/commit/49a4827))
## [0.75.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.75.1...v0.75.2) (2018-08-09)
### Bug Fixes
* **spec/dag:** fix wrong example output for sha3-512 hash algorithm ([#347](https://github.com/ipfs/interface-ipfs-core/issues/347)) ([bfdda8a](https://github.com/ipfs/interface-ipfs-core/commit/bfdda8a)), closes [#307](https://github.com/ipfs/interface-ipfs-core/issues/307)
* update error messages in line with go ([#348](https://github.com/ipfs/interface-ipfs-core/issues/348)) ([a173a42](https://github.com/ipfs/interface-ipfs-core/commit/a173a42))
## [0.75.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.75.0...v0.75.1) (2018-08-06)
### Bug Fixes
* ensure test for resolve recursive has another node ([#346](https://github.com/ipfs/interface-ipfs-core/issues/346)) ([09c2637](https://github.com/ipfs/interface-ipfs-core/commit/09c2637))
## [0.75.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.74.1...v0.75.0) (2018-08-06)
### Bug Fixes
* expect config to be an object ([#344](https://github.com/ipfs/interface-ipfs-core/issues/344)) ([eca00b9](https://github.com/ipfs/interface-ipfs-core/commit/eca00b9))
* more time for CI to resolve recursively ([79b747e](https://github.com/ipfs/interface-ipfs-core/commit/79b747e))
## [0.74.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.74.0...v0.74.1) (2018-08-06)
### Bug Fixes
* give more time for teardown after resolve ([#345](https://github.com/ipfs/interface-ipfs-core/issues/345)) ([1db498f](https://github.com/ipfs/interface-ipfs-core/commit/1db498f))
## [0.74.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.73.0...v0.74.0) (2018-08-02)
### Features
* **dht:** add API to allow options in `findprovs()` ([#337](https://github.com/ipfs/interface-ipfs-core/issues/337)) ([99f74f5](https://github.com/ipfs/interface-ipfs-core/commit/99f74f5)), closes [/github.com/ipfs/js-ipfs/issues/1322#issuecomment-385336102](https://github.com//github.com/ipfs/js-ipfs/issues/1322/issues/issuecomment-385336102)
## [0.73.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.72.1...v0.73.0) (2018-08-02)
## [0.72.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.72.0...v0.72.1) (2018-07-16)
### Bug Fixes
* unsubscribe in series for go-ipfs ([#326](https://github.com/ipfs/interface-ipfs-core/issues/326)) ([8e487da](https://github.com/ipfs/interface-ipfs-core/commit/8e487da))
## [0.72.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.71.0...v0.72.0) (2018-07-05)
## [0.71.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.70.3...v0.71.0) (2018-07-03)
### Bug Fixes
* revert to serialized pubsub operations ([#319](https://github.com/ipfs/interface-ipfs-core/issues/319)) ([4b5534e](https://github.com/ipfs/interface-ipfs-core/commit/4b5534e))
## [0.70.3](https://github.com/ipfs/interface-ipfs-core/compare/v0.70.2...v0.70.3) (2018-07-03)
### Bug Fixes
* allow passing only to suites with skip lists ([#321](https://github.com/ipfs/interface-ipfs-core/issues/321)) ([c47c4ce](https://github.com/ipfs/interface-ipfs-core/commit/c47c4ce))
* allow skip with object but no reason ([#318](https://github.com/ipfs/interface-ipfs-core/issues/318)) ([ef91026](https://github.com/ipfs/interface-ipfs-core/commit/ef91026))
* license ([#312](https://github.com/ipfs/interface-ipfs-core/issues/312)) ([8fa3e98](https://github.com/ipfs/interface-ipfs-core/commit/8fa3e98))
## [0.70.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.70.1...v0.70.2) (2018-06-29)
## [0.70.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.70.0...v0.70.1) (2018-06-27)
### Bug Fixes
* allow null skip for subsystems ([5df855c](https://github.com/ipfs/interface-ipfs-core/commit/5df855c))
## [0.70.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.69.1...v0.70.0) (2018-06-27)
### Features
* modularise tests by command, add tools to skip and only ([#290](https://github.com/ipfs/interface-ipfs-core/issues/290)) ([e232d8c](https://github.com/ipfs/interface-ipfs-core/commit/e232d8c))
### BREAKING CHANGES
* Consumers of this test suite now have fine grained control over what tests are run. Tests can now be skipped and "onlyed" (run only specific tests). This can be done on a test, command and sub-system level. See the updated usage guide for instructions: https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/README.md#usage.
This means that tests skips depending on implementation (e.g. go/js), environment (e.g. node/browser) or platform (e.g. macOS/linux/windows) that were previously present in this suite have been removed. Consumers of this library should add their own skips based on the implementation that's being tested and the environment/platform that the tests are running on.
The following other breaking changes have been made:
1. The common object passed to test suites has changed. It must now be a function that returns a common object (same shape and functions as before).
2. The `ipfs.ls` tests (not MFS `ipfs.files.ls`) is now a root level suite. You'll need to import it and use like `tests.ls(createCommon)` to have those tests run.
3. The `generic` suite (an alias to `miscellaneous`) has been removed.
See https://github.com/ipfs/interface-ipfs-core/pull/290 for more details.
License: MIT
Signed-off-by: Alan Shaw
## [0.69.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.69.0...v0.69.1) (2018-06-26)
### Bug Fixes
* do not rely on discovery for ping tests ([3acd6fd](https://github.com/ipfs/interface-ipfs-core/commit/3acd6fd)), closes [#310](https://github.com/ipfs/interface-ipfs-core/issues/310)
## [0.69.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.68.2...v0.69.0) (2018-06-22)
## [0.68.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.68.1...v0.68.2) (2018-06-19)
### Bug Fixes
* increase bitswap setup timeout for CI ([5886445](https://github.com/ipfs/interface-ipfs-core/commit/5886445))
## [0.68.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.68.0...v0.68.1) (2018-06-18)
### Bug Fixes
* removes error code checks for bitswap offline tests ([b152856](https://github.com/ipfs/interface-ipfs-core/commit/b152856))
## [0.68.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.67.0...v0.68.0) (2018-06-18)
### Bug Fixes
* improve bitswap wantlist and unwant docs ([7737546](https://github.com/ipfs/interface-ipfs-core/commit/7737546))
* linting errors ([fcc834c](https://github.com/ipfs/interface-ipfs-core/commit/fcc834c))
* removes duplicated TOC for pubsub ([a358cf7](https://github.com/ipfs/interface-ipfs-core/commit/a358cf7))
### Features
* add bitswap.unwant javascript spec ([df4e677](https://github.com/ipfs/interface-ipfs-core/commit/df4e677))
* add bitswap.unwant javascript spec ([d75a361](https://github.com/ipfs/interface-ipfs-core/commit/d75a361))
* add bitswap.unwant javascript spec ([c291ca9](https://github.com/ipfs/interface-ipfs-core/commit/c291ca9))
* add peerId param to bitswap.wantlist ([9f81bcb](https://github.com/ipfs/interface-ipfs-core/commit/9f81bcb))
## [0.67.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.66.4...v0.67.0) (2018-06-04)
## [0.66.4](https://github.com/ipfs/interface-ipfs-core/compare/v0.66.3...v0.66.4) (2018-05-30)
### Bug Fixes
* wait for put in object.patch.addLink before hook ([31c52d1](https://github.com/ipfs/interface-ipfs-core/commit/31c52d1))
## [0.66.3](https://github.com/ipfs/interface-ipfs-core/compare/v0.66.2...v0.66.3) (2018-05-25)
### Bug Fixes
* correctly differentiate pong responses ([688f4d7](https://github.com/ipfs/interface-ipfs-core/commit/688f4d7))
## [0.66.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.66.1...v0.66.2) (2018-05-18)
### Bug Fixes
* spawn in series ([d976699](https://github.com/ipfs/interface-ipfs-core/commit/d976699))
## [0.66.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.66.0...v0.66.1) (2018-05-17)
### Bug Fixes
* increase timeouts ([9cba111](https://github.com/ipfs/interface-ipfs-core/commit/9cba111))
* remove .only ([45fab1c](https://github.com/ipfs/interface-ipfs-core/commit/45fab1c))
* wait until nodes are connected before starting ping tests ([1b60f24](https://github.com/ipfs/interface-ipfs-core/commit/1b60f24))
* **pubsub:** clear interval on error ([d074e13](https://github.com/ipfs/interface-ipfs-core/commit/d074e13))
## [0.66.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.65.9...v0.66.0) (2018-05-16)
## [0.65.9](https://github.com/ipfs/interface-ipfs-core/compare/v0.65.8...v0.65.9) (2018-05-16)
### Bug Fixes
* add "files." to read* headers ([8b39b12](https://github.com/ipfs/interface-ipfs-core/commit/8b39b12))
* linting warnings ([aae31b0](https://github.com/ipfs/interface-ipfs-core/commit/aae31b0))
### Features
* add utils to spawn multiple nodes and get their ID ([e77a2f6](https://github.com/ipfs/interface-ipfs-core/commit/e77a2f6))
## [0.65.8](https://github.com/ipfs/interface-ipfs-core/compare/v0.65.7...v0.65.8) (2018-05-15)
## [0.65.7](https://github.com/ipfs/interface-ipfs-core/compare/v0.65.6...v0.65.7) (2018-05-15)
## [0.65.6](https://github.com/ipfs/interface-ipfs-core/compare/v0.65.5...v0.65.6) (2018-05-15)
## [0.65.5](https://github.com/ipfs/interface-ipfs-core/compare/v0.65.4...v0.65.5) (2018-05-12)
## [0.65.4](https://github.com/ipfs/interface-ipfs-core/compare/v0.65.3...v0.65.4) (2018-05-11)
## [0.65.3](https://github.com/ipfs/interface-ipfs-core/compare/v0.65.2...v0.65.3) (2018-05-11)
## [0.65.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.65.1...v0.65.2) (2018-05-11)
## [0.65.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.65.0...v0.65.1) (2018-05-11)
## [0.65.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.64.3...v0.65.0) (2018-05-11)
### Bug Fixes
* many fixes for pubsub tests with new async unsubscribe ([2019c45](https://github.com/ipfs/interface-ipfs-core/commit/2019c45))
* pubsub subscribe call with options ([c43f8bc](https://github.com/ipfs/interface-ipfs-core/commit/c43f8bc))
* remove .only ([251cffd](https://github.com/ipfs/interface-ipfs-core/commit/251cffd))
* remove duplicate async.each ([f798597](https://github.com/ipfs/interface-ipfs-core/commit/f798597))
## [0.64.3](https://github.com/ipfs/interface-ipfs-core/compare/v0.64.2...v0.64.3) (2018-05-06)
### Bug Fixes
* Typos on bundled libraries pull request ([2972426](https://github.com/ipfs/interface-ipfs-core/commit/2972426))
### Features
* add onlyHash option to files.add ([#259](https://github.com/ipfs/interface-ipfs-core/issues/259)) ([63179b9](https://github.com/ipfs/interface-ipfs-core/commit/63179b9))
### Performance Improvements
* **pubsub:** Change pubsub tests to do lighter load testing ([90a1520](https://github.com/ipfs/interface-ipfs-core/commit/90a1520))
## [0.64.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.64.1...v0.64.2) (2018-04-23)
## [0.64.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.64.0...v0.64.1) (2018-04-23)
### Bug Fixes
* this.skip needs to be under a function declaration ([2545ddd](https://github.com/ipfs/interface-ipfs-core/commit/2545ddd))
## [0.64.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.62.0...v0.64.0) (2018-04-23)
### Features
* adds pull stream tests for files.add ([d75986a](https://github.com/ipfs/interface-ipfs-core/commit/d75986a))
* better badge ([#246](https://github.com/ipfs/interface-ipfs-core/issues/246)) ([a3869bf](https://github.com/ipfs/interface-ipfs-core/commit/a3869bf))
## [0.63.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.62.0...v0.63.0) (2018-04-23)
### Features
* adds pull stream tests for files.add ([d75986a](https://github.com/ipfs/interface-ipfs-core/commit/d75986a))
## [0.62.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.61.0...v0.62.0) (2018-04-14)
## [0.61.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.60.1...v0.61.0) (2018-04-10)
## [0.60.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.60.0...v0.60.1) (2018-04-05)
### Bug Fixes
* fix wrapWithDirectory test ([a97c087](https://github.com/ipfs/interface-ipfs-core/commit/a97c087))
## [0.60.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.59.0...v0.60.0) (2018-04-05)
### Features
* Provide access to bundled libraries when in browser ([db83b50](https://github.com/ipfs/interface-ipfs-core/commit/db83b50))
## [0.59.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.58.0...v0.59.0) (2018-04-03)
### Features
* add wrapWithDirectory to files.add et al ([03eec9e](https://github.com/ipfs/interface-ipfs-core/commit/03eec9e))
## [0.58.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.57.0...v0.58.0) (2018-03-22)
### Bug Fixes
* wrong description ([bad70ac](https://github.com/ipfs/interface-ipfs-core/commit/bad70ac))
## [0.57.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.56.6...v0.57.0) (2018-03-16)
## [0.56.6](https://github.com/ipfs/interface-ipfs-core/compare/v0.56.5...v0.56.6) (2018-03-16)
## [0.56.5](https://github.com/ipfs/interface-ipfs-core/compare/v0.56.4...v0.56.5) (2018-03-16)
### Bug Fixes
* go-ipfs has not shipped withLocal yet ([58b1fe2](https://github.com/ipfs/interface-ipfs-core/commit/58b1fe2))
## [0.56.4](https://github.com/ipfs/interface-ipfs-core/compare/v0.56.3...v0.56.4) (2018-03-16)
## [0.56.3](https://github.com/ipfs/interface-ipfs-core/compare/v0.56.2...v0.56.3) (2018-03-16)
## [0.56.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.56.1...v0.56.2) (2018-03-16)
## [0.56.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.56.0...v0.56.1) (2018-03-16)
### Bug Fixes
* don't error to specific ([ec16016](https://github.com/ipfs/interface-ipfs-core/commit/ec16016))
* fix broken stat tests ([#236](https://github.com/ipfs/interface-ipfs-core/issues/236)) ([fcb8341](https://github.com/ipfs/interface-ipfs-core/commit/fcb8341)), closes [/github.com/ipfs/interface-ipfs-core/commit/c4934ca0b3b43f5bfc1ff5dd38f85d945d3244de#diff-0a6449ecfa8b9e3d807f53dde24eca71R66](https://github.com//github.com/ipfs/interface-ipfs-core/commit/c4934ca0b3b43f5bfc1ff5dd38f85d945d3244de/issues/diff-0a6449ecfa8b9e3d807f53dde24eca71R66)
## [0.56.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.55.1...v0.56.0) (2018-03-12)
### Features
* complete files.stat with the 'with-local' option ([#227](https://github.com/ipfs/interface-ipfs-core/issues/227)) ([5969fed](https://github.com/ipfs/interface-ipfs-core/commit/5969fed))
## [0.55.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.55.0...v0.55.1) (2018-03-09)
### Bug Fixes
* files.add accepts object ([88a635a](https://github.com/ipfs/interface-ipfs-core/commit/88a635a))
## [0.55.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.54.0...v0.55.0) (2018-03-09)
### Bug Fixes
* only skip if it is go-ipfs on Windows ([0df216f](https://github.com/ipfs/interface-ipfs-core/commit/0df216f))
## [0.54.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.53.0...v0.54.0) (2018-03-07)
### Bug Fixes
* fixes doc and adds test assertion that peer is a PeerId in return value from swarm.peers ([#230](https://github.com/ipfs/interface-ipfs-core/issues/230)) ([db530d7](https://github.com/ipfs/interface-ipfs-core/commit/db530d7))
## [0.53.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.52.0...v0.53.0) (2018-03-07)
### Bug Fixes
* adapt dag tests to current environment ([7a6fc5f](https://github.com/ipfs/interface-ipfs-core/commit/7a6fc5f))
* bwPullStream example ([59bd7ac](https://github.com/ipfs/interface-ipfs-core/commit/59bd7ac))
## [0.52.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.51.0...v0.52.0) (2018-02-15)
### Features
* allow stats tests to run on js-ipfs ([#216](https://github.com/ipfs/interface-ipfs-core/issues/216)) ([f6e5f55](https://github.com/ipfs/interface-ipfs-core/commit/f6e5f55))
## [0.51.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.50.1...v0.51.0) (2018-02-15)
### Bug Fixes
* bootstrap add test ([df01cc5](https://github.com/ipfs/interface-ipfs-core/commit/df01cc5))
### Features
* **bootstrap:** add the spec ([427338e](https://github.com/ipfs/interface-ipfs-core/commit/427338e))
## [0.50.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.50.0...v0.50.1) (2018-02-14)
### Bug Fixes
* add pointer to files-mfs tests ([6bc22c9](https://github.com/ipfs/interface-ipfs-core/commit/6bc22c9))
## [0.50.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.49.2...v0.50.0) (2018-02-14)
### Features
* factor out mfs tests to separate file ([91666ca](https://github.com/ipfs/interface-ipfs-core/commit/91666ca))
## [0.49.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.49.1...v0.49.2) (2018-02-14)
### Bug Fixes
* remove unnecessary console.log ([e27d3e0](https://github.com/ipfs/interface-ipfs-core/commit/e27d3e0))
## [0.49.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.49.0...v0.49.1) (2018-02-12)
### Bug Fixes
* remove .only ([44cdaed](https://github.com/ipfs/interface-ipfs-core/commit/44cdaed))
## [0.49.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.48.0...v0.49.0) (2018-02-12)
### Bug Fixes
* use latest fixture loading ([#218](https://github.com/ipfs/interface-ipfs-core/issues/218)) ([e054097](https://github.com/ipfs/interface-ipfs-core/commit/e054097))
## [0.48.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.47.0...v0.48.0) (2018-02-07)
## [0.47.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.46.0...v0.47.0) (2018-02-07)
### Features
* add stats.bwPullStream and stats.bwReadableStream ([#211](https://github.com/ipfs/interface-ipfs-core/issues/211)) ([4421eb2](https://github.com/ipfs/interface-ipfs-core/commit/4421eb2))
## [0.46.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.44.0...v0.46.0) (2018-02-02)
## [0.45.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.44.0...v0.45.0) (2018-02-02)
## [0.44.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.43.0...v0.44.0) (2018-02-02)
### Features
* ipfs.shutdown test ([#214](https://github.com/ipfs/interface-ipfs-core/issues/214)) ([e911c6c](https://github.com/ipfs/interface-ipfs-core/commit/e911c6c))
* Link stats.repo and stats.bitswap ([#210](https://github.com/ipfs/interface-ipfs-core/issues/210)) ([0c40084](https://github.com/ipfs/interface-ipfs-core/commit/0c40084))
* shutdown spec ([9d91267](https://github.com/ipfs/interface-ipfs-core/commit/9d91267))
## [0.43.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.42.1...v0.43.0) (2018-01-25)
## [0.42.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.42.0...v0.42.1) (2018-01-25)
### Bug Fixes
* stats not implemented on jsipfs ([#209](https://github.com/ipfs/interface-ipfs-core/issues/209)) ([af32ecf](https://github.com/ipfs/interface-ipfs-core/commit/af32ecf))
## [0.42.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.41.1...v0.42.0) (2018-01-25)
### Bug Fixes
* Update PUBSUB.md ([#204](https://github.com/ipfs/interface-ipfs-core/issues/204)) ([0409e3a](https://github.com/ipfs/interface-ipfs-core/commit/0409e3a))
### Features
* add stats spec ([220483f](https://github.com/ipfs/interface-ipfs-core/commit/220483f))
* REPO spec ([#207](https://github.com/ipfs/interface-ipfs-core/issues/207)) ([803a3ef](https://github.com/ipfs/interface-ipfs-core/commit/803a3ef))
* spec MFS Actions ([#206](https://github.com/ipfs/interface-ipfs-core/issues/206)) ([7431098](https://github.com/ipfs/interface-ipfs-core/commit/7431098))
## [0.41.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.41.0...v0.41.1) (2018-01-19)
### Bug Fixes
* Revert "feat: use new ipfsd-ctl ([#186](https://github.com/ipfs/interface-ipfs-core/issues/186))" ([#203](https://github.com/ipfs/interface-ipfs-core/issues/203)) ([67b74a3](https://github.com/ipfs/interface-ipfs-core/commit/67b74a3))
## [0.41.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.40.0...v0.41.0) (2018-01-19)
### Features
* use new ipfsd-ctl ([#186](https://github.com/ipfs/interface-ipfs-core/issues/186)) ([4d4ef7f](https://github.com/ipfs/interface-ipfs-core/commit/4d4ef7f))
## [0.40.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.39.0...v0.40.0) (2018-01-12)
## [0.39.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.38.0...v0.39.0) (2018-01-10)
## [0.38.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.37.0...v0.38.0) (2018-01-05)
### Features
* normalize KEY API ([#192](https://github.com/ipfs/interface-ipfs-core/issues/192)) ([5a21d6c](https://github.com/ipfs/interface-ipfs-core/commit/5a21d6c))
* normalize NAME API ([#190](https://github.com/ipfs/interface-ipfs-core/issues/190)) ([9670c1a](https://github.com/ipfs/interface-ipfs-core/commit/9670c1a))
## [0.37.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.16...v0.37.0) (2017-12-28)
## [0.36.16](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.15...v0.36.16) (2017-12-18)
### Bug Fixes
* key.rm test ([#185](https://github.com/ipfs/interface-ipfs-core/issues/185)) ([211e2c5](https://github.com/ipfs/interface-ipfs-core/commit/211e2c5))
## [0.36.15](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.14...v0.36.15) (2017-12-12)
### Bug Fixes
* cat not found message in go-ipfs ([#183](https://github.com/ipfs/interface-ipfs-core/issues/183)) ([8e3645e](https://github.com/ipfs/interface-ipfs-core/commit/8e3645e))
## [0.36.14](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.13...v0.36.14) (2017-12-12)
## [0.36.13](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.12...v0.36.13) (2017-12-10)
### Features
* key tests ([#180](https://github.com/ipfs/interface-ipfs-core/issues/180)) ([b75e13b](https://github.com/ipfs/interface-ipfs-core/commit/b75e13b))
## [0.36.12](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.11...v0.36.12) (2017-12-05)
## [0.36.11](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.10...v0.36.11) (2017-11-26)
## [0.36.10](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.9...v0.36.10) (2017-11-25)
## [0.36.9](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.8...v0.36.9) (2017-11-23)
## [0.36.8](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.7...v0.36.8) (2017-11-22)
### Bug Fixes
* **pubsub:** swarm connect to local servers ([#175](https://github.com/ipfs/interface-ipfs-core/issues/175)) ([09d9573](https://github.com/ipfs/interface-ipfs-core/commit/09d9573))
## [0.36.7](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.6...v0.36.7) (2017-11-20)
## [0.36.6](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.4...v0.36.6) (2017-11-20)
## [0.36.5](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.4...v0.36.5) (2017-11-20)
## [0.36.4](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.3...v0.36.4) (2017-11-17)
## [0.36.3](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.2...v0.36.3) (2017-11-17)
## [0.36.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.1...v0.36.2) (2017-11-17)
## [0.36.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.36.0...v0.36.1) (2017-11-17)
## [0.36.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.35.0...v0.36.0) (2017-11-17)
## [0.35.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.34.3...v0.35.0) (2017-11-16)
### Bug Fixes
* **pubsub:** topicCIDs should be topicIDs ([#169](https://github.com/ipfs/interface-ipfs-core/issues/169)) ([d357f5f](https://github.com/ipfs/interface-ipfs-core/commit/d357f5f))
## [0.34.3](https://github.com/ipfs/interface-ipfs-core/compare/v0.34.2...v0.34.3) (2017-11-14)
## [0.34.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.34.0...v0.34.2) (2017-11-13)
## [0.34.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.34.0...v0.34.1) (2017-11-13)
## [0.34.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.33.2...v0.34.0) (2017-11-13)
## [0.33.2](https://github.com/ipfs/interface-ipfs-core/compare/v0.33.1...v0.33.2) (2017-11-09)
### Bug Fixes
* **package:** aegir is a dependency ([#166](https://github.com/ipfs/interface-ipfs-core/issues/166)) ([72f2f56](https://github.com/ipfs/interface-ipfs-core/commit/72f2f56))
## [0.33.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.33.0...v0.33.1) (2017-10-22)
## [0.33.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.32.1...v0.33.0) (2017-10-22)
## [0.32.1](https://github.com/ipfs/interface-ipfs-core/compare/v0.32.0...v0.32.1) (2017-10-18)
### Bug Fixes
* make tests consistent across js-ipfs/go-ipfs ([#158](https://github.com/ipfs/interface-ipfs-core/issues/158)) ([a5a4c37](https://github.com/ipfs/interface-ipfs-core/commit/a5a4c37))
## [0.32.0](https://github.com/ipfs/interface-ipfs-core/compare/v0.31.19...v0.32.0) (2017-10-18)
### Features
* add progress bar tests ([#155](https://github.com/ipfs/interface-ipfs-core/issues/155)) ([fad3fa2](https://github.com/ipfs/interface-ipfs-core/commit/fad3fa2))
## [0.31.19](https://github.com/ipfs/interface-ipfs-core/compare/v0.31.18...v0.31.19) (2017-09-04)
### Bug Fixes
* remove superfluous console.logs ([442ea74](https://github.com/ipfs/interface-ipfs-core/commit/442ea74))
================================================
FILE: packages/interface-ipfs-core/LICENSE
================================================
This project is dual licensed under MIT and Apache-2.0.
MIT: https://www.opensource.org/licenses/mit
Apache-2.0: https://www.apache.org/licenses/license-2.0
================================================
FILE: packages/interface-ipfs-core/LICENSE-APACHE
================================================
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
================================================
FILE: packages/interface-ipfs-core/LICENSE-MIT
================================================
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: packages/interface-ipfs-core/README.md
================================================
> # ⛔️ DEPRECATED: [js-IPFS](https://github.com/ipfs/js-ipfs) has been superseded by [Helia](https://github.com/ipfs/helia)
>
> 📚 [Learn more about this deprecation](https://github.com/ipfs/js-ipfs/issues/4336) or [how to migrate](https://github.com/ipfs/helia/wiki/Migrating-from-js-IPFS)
>
> ⚠️ If you continue using this repo, please note that security fixes will not be provided
# interface-ipfs-core
[](https://ipfs.tech)
[](https://discuss.ipfs.tech)
[](https://codecov.io/gh/ipfs/js-ipfs)
[](https://github.com/ipfs/js-ipfs/actions/workflows/test.yml?query=branch%3Amaster)
> A test suite and interface you can use to implement a IPFS core interface.
## Table of contents
- [Install](#install)
- [Background](#background)
- [Core API](#core-api)
- [Modules that implement the interface](#modules-that-implement-the-interface)
- [Badge](#badge)
- [Usage](#usage)
- [Running tests](#running-tests)
- [Running tests by command](#running-tests-by-command)
- [Running only some tests](#running-only-some-tests)
- [Running only specific tests](#running-only-specific-tests)
- [Skipping tests](#skipping-tests)
- [Skipping specific tests](#skipping-specific-tests)
- [License](#license)
- [Contribute](#contribute)
## Install
```console
$ npm i interface-ipfs-core
```
## Background
The primary goal of this module is to define and ensure that IPFS core implementations and their respective HTTP client libraries offer the same interface, so that developers can quickly change between a local and a remote node without having to change their applications.
It offers a suite of tests that can be run in order to check if the interface is implemented as described.
## Core API
In order to be considered "valid", an IPFS implementation must expose the Core API as described in [/docs/core-api](https://github.com/ipfs/js-ipfs/tree/master/docs/core-api). You can also use this loose spec as documentation for consuming the core APIs.
## Modules that implement the interface
- [JavaScript IPFS implementation](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs)
- [JavaScript IPFS HTTP Client Library](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs-http-client)
- [JavaScript IPFS postMessage proxy](https://github.com/ipfs-shipyard/ipfs-postmsg-proxy)
Send in a PR if you find or write one!
## Badge
Include this badge in your readme if you make a new module that implements interface-ipfs-core API.

```md
[](https://github.com/ipfs/js-ipfs/tree/master/packages/interface-ipfs-core)
```
```console
$ npm install interface-ipfs-core
```
If you want to run these tests against a Kubo daemon, checkout [ipfs-http-client](https://github.com/ipfs/js-ipfs-http-client) and run test tests:
```console
$ git clone https://github.com/ipfs/js-ipfs-http-client
$ npm install
$ npm test
```
## Usage
Install `interface-ipfs-core` as one of the dependencies of your project and as a test file. Then, using `mocha` (for Node.js) or a test runner with compatible API, do:
```js
import * as tests from 'interface-ipfs-core'
const nodes = []
// Create common setup and teardown
const createCommon = () => ({
// Do some setup common to all tests
setup: async () => {
// Use ipfsd-ctl or other to spawn an IPFS node for testing
const node = await spawnNode()
nodes.push(node)
return node.api
},
// Dispose of nodes created by the IPFS factory and any other teardown
teardown: () => {
return Promise.all(nodes.map(n => n.stop()))
}
})
tests.block(createCommon)
tests.config(createCommon)
tests.dag(createCommon)
// ...etc. (see src/index.js)
```
## Running tests
```js
// run all the tests for the repo subsystem
tests.repo(createCommon)
```
### Running tests by command
```js
tests.repo.version(createCommon)
```
### Running only some tests
```js
tests.repo.gc(createCommon, { only: true }) // pass an options object to run only these tests
// OR, at the subsystem level
// runs only ALL the repo.gc tests
tests.repo(createCommon, { only: ['gc'] })
// runs only ALL the object.patch.addLink tests
tests.object(createCommon, { only: ['patch.addLink'] })
```
### Running only specific tests
```js
tests.repo.gc(createCommon, { only: ['should do a thing'] }) // only run these named test(s)
// OR, at the subsystem level
tests.repo(createCommon, { only: ['should do a thing'] })
```
## Skipping tests
```js
tests.repo.gc(createCommon, { skip: true }) // pass an options object to skip these tests
// skips ALL the repo.gc tests
tests.repo(createCommon, { skip: ['gc'] })
// skips ALL the object.patch.addLink tests
tests.object(createCommon, { skip: ['patch.addLink'] })
```
### Skipping specific tests
```js
tests.repo.gc(createCommon, { skip: ['should do a thing'] }) // named test(s) to skip
// OR, at the subsystem level
tests.repo(createCommon, { skip: ['should do a thing'] })
// Optionally specify a reason
tests.repo(createCommon, {
skip: [{
name: 'should do a thing',
reason: 'Thing is not implemented yet'
}]
})
```
## License
Licensed under either of
- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / )
- MIT ([LICENSE-MIT](LICENSE-MIT) / )
## Contribute
Contributions welcome! Please check out [the issues](https://github.com/ipfs/js-ipfs/issues).
Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general.
Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md).
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
[](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md)
[UnixFS]: https://github.com/ipfs/specs/tree/master/unixfs
================================================
FILE: packages/interface-ipfs-core/maintainer.json
================================================
{
"repoLeadMaintainer": {
"name": "Alan Shaw",
"email": "alan.shaw@protocol.ai",
"username": "alanshaw"
},
"workingGroup": {
"name": "JS IPFS",
"entryPoint": "https://github.com/ipfs/js-core"
}
}
================================================
FILE: packages/interface-ipfs-core/package.json
================================================
{
"name": "interface-ipfs-core",
"version": "0.158.1",
"description": "A test suite and interface you can use to implement a IPFS core interface.",
"license": "Apache-2.0 OR MIT",
"homepage": "https://github.com/ipfs/js-ipfs/tree/master/packages/interface-ipfs-core#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/ipfs/js-ipfs.git"
},
"bugs": {
"url": "https://github.com/ipfs/js-ipfs/issues"
},
"keywords": [
"IPFS"
],
"engines": {
"node": ">=16.0.0",
"npm": ">=7.0.0"
},
"type": "module",
"types": "./dist/src/index.d.ts",
"typesVersions": {
"*": {
"*": [
"*",
"dist/*",
"dist/src/*",
"dist/src/*/index"
],
"src/*": [
"*",
"dist/*",
"dist/src/*",
"dist/src/*/index"
]
}
},
"files": [
"src",
"dist",
"!dist/test",
"!**/*.tsbuildinfo"
],
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./src/index.js"
}
},
"eslintConfig": {
"extends": "ipfs",
"parserOptions": {
"sourceType": "module"
},
"ignorePatterns": [
"test/fixtures/*"
]
},
"scripts": {
"clean": "aegir clean",
"build": "aegir build && copyfiles './test/fixtures/**/*' ./dist",
"lint": "aegir lint",
"dep-check": "aegir dep-check -i ipfs-core-types -i copyfiles -i @libp2p/interfaces"
},
"dependencies": {
"@ipld/car": "^5.0.0",
"@ipld/dag-cbor": "^9.0.0",
"@ipld/dag-pb": "^4.0.0",
"@libp2p/crypto": "^1.0.7",
"@libp2p/interface-peer-id": "^2.0.0",
"@libp2p/interfaces": "^3.2.0",
"@libp2p/peer-id": "^2.0.0",
"@libp2p/peer-id-factory": "^2.0.0",
"@libp2p/websockets": "^5.0.0",
"@multiformats/multiaddr": "^11.1.5",
"@types/node": "^18.0.0",
"@types/pako": "^2.0.0",
"@types/readable-stream": "^2.3.13",
"aegir": "^37.11.0",
"blockstore-core": "^3.0.0",
"copyfiles": "^2.4.1",
"dag-jose": "^4.0.0",
"delay": "^5.0.0",
"did-jwt": "^6.2.0",
"err-code": "^3.0.1",
"ipfs-core-types": "^0.14.1",
"ipfs-unixfs": "^9.0.0",
"ipfs-unixfs-importer": "^12.0.0",
"ipfs-utils": "^9.0.13",
"ipns": "^5.0.1",
"is-ipfs": "^8.0.0",
"iso-random-stream": "^2.0.2",
"it-all": "^2.0.0",
"it-buffer-stream": "^3.0.0",
"it-concat": "^3.0.1",
"it-drain": "^2.0.0",
"it-first": "^2.0.0",
"it-last": "^2.0.0",
"it-map": "^2.0.0",
"it-pipe": "^2.0.3",
"it-pushable": "^3.0.0",
"it-tar": "^6.0.0",
"it-to-buffer": "^3.0.0",
"merge-options": "^3.0.4",
"multiformats": "^11.0.0",
"nanoid": "^4.0.0",
"p-defer": "^4.0.0",
"p-map": "^5.3.0",
"p-retry": "^5.1.0",
"p-wait-for": "^5.0.0",
"pako": "^2.0.4",
"readable-stream": "^4.0.0",
"sinon": "^15.0.1",
"stream": "^0.0.2",
"uint8arrays": "^4.0.2",
"wherearewe": "^2.0.1"
},
"browser": {
"fs": false,
"os": false,
"path": false
}
}
================================================
FILE: packages/interface-ipfs-core/src/add-all.js
================================================
/* eslint-env mocha, browser */
import { fixtures } from './utils/index.js'
import { Readable } from 'readable-stream'
import all from 'it-all'
import last from 'it-last'
import drain from 'it-drain'
import { supportsFileReader } from 'ipfs-utils/src/supports.js'
import globSource from 'ipfs-utils/src/files/glob-source.js'
import { isNode } from 'ipfs-utils/src/env.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from './utils/mocha.js'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import bufferStream from 'it-buffer-stream'
import * as raw from 'multiformats/codecs/raw'
import * as dagPB from '@ipld/dag-pb'
import resolve from 'aegir/resolve'
import { sha256, sha512 } from 'multiformats/hashes/sha2'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
* @typedef {import('ipfs-unixfs').MtimeLike} MtimeLike
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testAddAll (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.addAll', function () {
this.timeout(360 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/**
* @param {string | number} mode
* @param {number} expectedMode
*/
async function testMode (mode, expectedMode) {
const content = String(Math.random() + Date.now())
const files = await all(ipfs.addAll([{
content: uint8ArrayFromString(content),
mode
}]))
expect(files).to.have.length(1)
expect(files).to.have.nested.property('[0].mode', expectedMode)
const stats = await ipfs.files.stat(`/ipfs/${files[0].cid}`)
expect(stats).to.have.property('mode', expectedMode)
}
/**
* @param {MtimeLike} mtime
* @param {MtimeLike} expectedMtime
*/
async function testMtime (mtime, expectedMtime) {
const content = String(Math.random() + Date.now())
const files = await all(ipfs.addAll([{
content: uint8ArrayFromString(content),
mtime
}]))
expect(files).to.have.length(1)
expect(files).to.have.deep.nested.property('[0].mtime', expectedMtime)
const stats = await ipfs.files.stat(`/ipfs/${files[0].cid}`)
expect(stats).to.have.deep.property('mtime', expectedMtime)
}
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('should add a File as array of tuples', async function () {
if (!supportsFileReader) {
return this.skip()
}
const tuple = {
path: 'filename.txt',
content: new self.File(['should add a File'], 'filename.txt', { type: 'text/plain' })
}
const filesAdded = await all(ipfs.addAll([tuple]))
expect(filesAdded[0].cid.toString()).to.be.eq('QmTVfLxf3qXiJgr4KwG6UBckcNvTqBp93Rwy5f7h3mHsVC')
})
it('should add a Uint8Array as array of tuples', async () => {
const tuple = { path: 'testfile.txt', content: fixtures.smallFile.data }
const filesAdded = await all(ipfs.addAll([tuple]))
expect(filesAdded).to.have.length(1)
const file = filesAdded[0]
expect(file.cid.toString()).to.equal(fixtures.smallFile.cid.toString())
expect(file.path).to.equal('testfile.txt')
})
it('should add array of objects with readable stream content', async function () {
if (!isNode) {
this.skip()
}
const expectedCid = 'QmVv4Wz46JaZJeH5PMV4LGbRiiMKEmszPYY3g6fjGnVXBS'
const rs = new Readable()
rs.push(uint8ArrayFromString('some data'))
rs.push(null)
const tuple = { path: 'data.txt', content: rs }
const filesAdded = await all(ipfs.addAll([tuple]))
expect(filesAdded).to.be.length(1)
const file = filesAdded[0]
expect(file.path).to.equal('data.txt')
expect(file.size).to.equal(17)
expect(file.cid.toString()).to.equal(expectedCid)
})
it('should add a nested directory as array of tupples', async function () {
/**
* @param {string} name
*/
const content = (name) => ({
path: `test-folder/${name}`,
content: fixtures.directory.files[name]
})
/**
* @param {string} name
*/
const emptyDir = (name) => ({ path: `test-folder/${name}` })
const dirs = [
content('pp.txt'),
content('holmes.txt'),
content('jungle.txt'),
content('alice.txt'),
emptyDir('empty-folder'),
content('files/hello.txt'),
content('files/ipfs.txt'),
emptyDir('files/empty')
]
const root = await last(ipfs.addAll(dirs))
if (!root) {
throw new Error('Dirs were not loaded')
}
expect(root.path).to.equal('test-folder')
expect(root.cid.toString()).to.equal(fixtures.directory.cid.toString())
})
it('should add a nested directory as array of tupples with progress', async function () {
/**
* @param {string} name
*/
const content = (name) => ({
path: `test-folder/${name}`,
content: fixtures.directory.files[name]
})
/**
* @param {string} name
*/
const emptyDir = (name) => ({ path: `test-folder/${name}`, content: undefined })
/** @type {Record} */
const progressSizes = {}
const dirs = [
content('pp.txt'),
content('holmes.txt'),
content('jungle.txt'),
content('alice.txt'),
emptyDir('empty-folder'),
content('files/hello.txt'),
content('files/ipfs.txt'),
emptyDir('files/empty')
]
const total = dirs.reduce((/** @type {Record} */ acc, curr) => {
if (curr.content) {
acc[curr.path] = curr.content.length
}
return acc
}, {})
/**
* @type {import('ipfs-core-types/src/root').AddProgressFn}
*/
const handler = (bytes, path) => {
if (path) {
progressSizes[path] = bytes
}
}
const root = await last(ipfs.addAll(dirs, { progress: handler }))
expect(progressSizes).to.deep.equal(total)
expect(root).to.have.property('path', 'test-folder')
expect(root).to.have.deep.property('cid', fixtures.directory.cid)
})
it('should receive progress path as empty string when adding content without paths', async function () {
/**
* @param {string} name
*/
const content = (name) => fixtures.directory.files[name]
/** @type {Record} */
const progressSizes = {}
const dirs = [
content('pp.txt'),
content('holmes.txt'),
content('jungle.txt')
]
const total = {
'': dirs.reduce((acc, curr) => acc + curr.length, 0)
}
/**
* @type {import('ipfs-core-types/src/root').AddProgressFn}
*/
const handler = (bytes, path) => {
progressSizes[`${path}`] = bytes
}
await drain(ipfs.addAll(dirs, { progress: handler }))
expect(progressSizes).to.deep.equal(total)
})
it('should receive file name from progress event', async () => {
/** @type {string[]} */
const receivedNames = []
/**
* @type {import('ipfs-core-types/src/root').AddProgressFn}
*/
function handler (p, name) {
if (name) {
receivedNames.push(name)
}
}
await drain(ipfs.addAll([{
content: 'hello',
path: 'foo.txt'
}, {
content: 'world',
path: 'bar.txt'
}], {
progress: handler,
wrapWithDirectory: true
}))
expect(receivedNames).to.deep.equal(['foo.txt', 'bar.txt'])
})
it('should add files to a directory non sequentially', async function () {
/**
* @param {string} path
*/
const content = path => ({
path: `test-dir/${path}`,
content: fixtures.directory.files[path.split('/').pop() || '']
})
const input = [
content('a/pp.txt'),
content('a/holmes.txt'),
content('b/jungle.txt'),
content('a/alice.txt')
]
const filesAdded = await all(ipfs.addAll(input))
/**
* @param {object} arg
* @param {string} [arg.path]
*/
const toPath = ({ path }) => path
const nonSeqDirFilePaths = input.map(toPath).filter(p => p && p.includes('/a/'))
const filesAddedPaths = filesAdded.map(toPath)
expect(nonSeqDirFilePaths.every(p => filesAddedPaths.includes(p))).to.be.true()
})
it('should fail when passed invalid input', async () => {
const nonValid = 138
// @ts-expect-error nonValid is the wrong type
await expect(all(ipfs.addAll(nonValid))).to.eventually.be.rejected()
})
it('should fail when passed single file objects', async () => {
const nonValid = { content: 'hello world' }
// @ts-expect-error nonValid is non valid
await expect(all(ipfs.addAll(nonValid))).to.eventually.be.rejectedWith(/single item passed/)
})
it('should fail when passed single strings', async () => {
const nonValid = 'hello world'
await expect(all(ipfs.addAll(nonValid))).to.eventually.be.rejectedWith(/single item passed/)
})
it('should fail when passed single buffers', async () => {
const nonValid = uint8ArrayFromString('hello world')
// @ts-expect-error nonValid is non valid
await expect(all(ipfs.addAll(nonValid))).to.eventually.be.rejectedWith(/single item passed/)
})
it('should wrap content in a directory', async () => {
const data = { path: 'testfile.txt', content: fixtures.smallFile.data }
const filesAdded = await all(ipfs.addAll([data], { wrapWithDirectory: true }))
expect(filesAdded).to.have.length(2)
const file = filesAdded[0]
const wrapped = filesAdded[1]
expect(file.cid.toString()).to.equal(fixtures.smallFile.cid.toString())
expect(file.path).to.equal('testfile.txt')
expect(wrapped.path).to.equal('')
})
it('should add a directory with only-hash=true', async function () {
this.slow(10 * 1000)
const content = String(Math.random() + Date.now())
const files = await all(ipfs.addAll([{
path: '/foo/bar.txt',
content: uint8ArrayFromString(content)
}, {
path: '/foo/baz.txt',
content: uint8ArrayFromString(content)
}], { onlyHash: true }))
expect(files).to.have.length(3)
await Promise.all(
files.map(file => expect(ipfs.object.get(file.cid, { timeout: 4000 }))
.to.eventually.be.rejected()
.and.to.have.property('name').that.equals('TimeoutError')
)
)
})
it('should add with mode as string', async function () {
this.slow(10 * 1000)
const mode = '0777'
await testMode(mode, parseInt(mode, 8))
})
it('should add with mode as number', async function () {
this.slow(10 * 1000)
const mode = parseInt('0777', 8)
await testMode(mode, mode)
})
it('should add with mtime as Date', async function () {
this.slow(10 * 1000)
const mtime = new Date(5000)
await testMtime(mtime, {
secs: 5,
nsecs: 0
})
})
it('should add with mtime as { nsecs, secs }', async function () {
this.slow(10 * 1000)
const mtime = {
secs: 5,
nsecs: 0
}
await testMtime(mtime, mtime)
})
it('should add with mtime as timespec', async function () {
this.slow(10 * 1000)
await testMtime({
Seconds: 5,
FractionalNanoseconds: 0
}, {
secs: 5,
nsecs: 0
})
})
it('should add with mtime as hrtime', async function () {
this.slow(10 * 1000)
const mtime = process.hrtime()
await testMtime(mtime, {
secs: mtime[0],
nsecs: mtime[1]
})
})
it('should add a directory from the file system', async function () {
if (!isNode) this.skip()
const filesPath = resolve('test/fixtures/test-folder', 'interface-ipfs-core')
const result = await all(ipfs.addAll(globSource(filesPath, '**/*')))
expect(result.length).to.be.above(8)
})
it('should add a directory from the file system with an odd name', async function () {
if (!isNode) this.skip()
const filesPath = resolve('test/fixtures/weird name folder [v0]', 'interface-ipfs-core')
const result = await all(ipfs.addAll(globSource(filesPath, '**/*')))
expect(result.length).to.be.above(8)
})
it('should ignore a directory from the file system', async function () {
if (!isNode) this.skip()
const filesPath = resolve('test/fixtures/test-folder', 'interface-ipfs-core')
const result = await all(ipfs.addAll(globSource(filesPath, '@(!(files*))')))
expect(result.length).to.equal(6)
})
it('should add a file from the file system', async function () {
if (!isNode) this.skip()
const filePath = resolve('test/fixtures/test-folder', 'interface-ipfs-core')
const result = await all(ipfs.addAll(globSource(filePath, 'ipfs-add.js')))
expect(result.length).to.equal(1)
expect(result[0].path).to.equal('ipfs-add.js')
})
it('should add a hidden file in a directory from the file system', async function () {
if (!isNode) this.skip()
const filesPath = resolve('test/fixtures', 'interface-ipfs-core')
const result = await all(ipfs.addAll(globSource(filesPath, 'hidden-files-folder/**/*', { hidden: true })))
expect(result.map(object => object.path)).to.include('hidden-files-folder/.hiddenTest.txt')
expect(result.map(object => object.cid.toString())).to.include('QmdbAjVmLRdpFyi8FFvjPfhTGB2cVXvWLuK7Sbt38HXrtt')
})
it('should add a file with only-hash=true', async function () {
if (!isNode) this.skip()
this.slow(10 * 1000)
const out = await all(ipfs.addAll([{
content: uint8ArrayFromString('hello world')
}], { onlyHash: true }))
await expect(ipfs.object.get(out[0].cid, { timeout: 500 }))
.to.eventually.be.rejected()
.and.to.have.property('name').that.equals('TimeoutError')
})
it('should add all with sha2-256 by default', async function () {
const content = String(Math.random() + Date.now())
const files = await all(ipfs.addAll([content]))
expect(files).to.have.nested.property('[0].cid.multihash.code', sha256.code)
})
it('should add all with a different hashing algorithm', async function () {
const content = String(Math.random() + Date.now())
const files = await all(ipfs.addAll([content], { hashAlg: 'sha2-512' }))
expect(files).to.have.nested.property('[0].cid.multihash.code', sha512.code)
})
it('should respect raw leaves when file is smaller than one block and no metadata is present', async () => {
const files = await all(ipfs.addAll([Uint8Array.from([0, 1, 2])], {
cidVersion: 1,
rawLeaves: true
}))
expect(files.length).to.equal(1)
expect(files[0].cid.toString()).to.equal('bafkreifojmzibzlof6xyh5auu3r5vpu5l67brf3fitaf73isdlglqw2t7q')
expect(files[0].cid.code).to.equal(raw.code)
expect(files[0].size).to.equal(3)
})
it('should override raw leaves when file is smaller than one block and metadata is present', async () => {
const files = await all(ipfs.addAll([{
content: Uint8Array.from([0, 1, 2]),
mode: 0o123,
mtime: {
secs: 1000,
nsecs: 0
}
}], {
cidVersion: 1,
rawLeaves: true
}))
expect(files.length).to.equal(1)
expect(files[0].cid.toString()).to.equal('bafybeifmayxiu375ftlgydntjtffy5cssptjvxqw6vyuvtymntm37mpvua')
expect(files[0].cid.code).to.equal(dagPB.code)
expect(files[0].size).to.equal(18)
})
it('should add directories with metadata', async () => {
const files = await all(ipfs.addAll([{
path: '/foo',
mode: 0o123,
mtime: {
secs: 1000,
nsecs: 0
}
}]))
expect(files.length).to.equal(1)
expect(files[0].cid.toString()).to.equal('QmaZTosBmPwo9LQ48ESPCEcNuX2kFxkpXYy8i3rxqBdzRG')
expect(files[0].cid.code).to.equal(dagPB.code)
expect(files[0].size).to.equal(11)
})
it('should support bidirectional streaming', async function () {
let progressInvoked = false
/**
* @type {import('ipfs-core-types/src/root').AddProgressFn}
*/
const handler = (bytes, path) => {
progressInvoked = true
}
const source = async function * () {
yield {
content: 'hello',
path: '/file'
}
await new Promise((resolve) => {
const interval = setInterval(() => {
// we've received a progress result, that means we've received some
// data from the server before we're done sending data to the server
// so the streaming is bidirectional and we can finish up
if (progressInvoked) {
clearInterval(interval)
resolve(null)
}
}, 10)
})
}
await drain(ipfs.addAll(source(), {
progress: handler,
fileImportConcurrency: 1
}))
expect(progressInvoked).to.be.true()
})
it('should error during add-all stream', async function () {
const source = async function * () {
yield {
content: 'hello',
path: '/file'
}
yield {
content: 'hello',
path: '/file'
}
}
await expect(drain(ipfs.addAll(source(), {
fileImportConcurrency: 1,
chunker: 'rabin-2048--50' // invalid chunker parameters, validated after the stream starts moving
}))).to.eventually.be.rejectedWith(/Chunker parameter avg must be an integer/)
})
it('should add big files', async function () {
const totalSize = 1024 * 1024 * 200
const chunkSize = 1024 * 1024 * 99
const source = async function * () {
yield {
path: '/dir/file-200mb-1',
content: bufferStream(totalSize, {
chunkSize
})
}
yield {
path: '/dir/file-200mb-2',
content: bufferStream(totalSize, {
chunkSize
})
}
}
const results = await all(ipfs.addAll(source()))
expect(await ipfs.files.stat(`/ipfs/${results[0].cid}`)).to.have.property('size', totalSize)
expect(await ipfs.files.stat(`/ipfs/${results[1].cid}`)).to.have.property('size', totalSize)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/add.js
================================================
/* eslint-env mocha, browser */
import { fixtures } from './utils/index.js'
import { Readable } from 'readable-stream'
import { supportsFileReader } from 'ipfs-utils/src/supports.js'
import urlSource from 'ipfs-utils/src/files/url-source.js'
import { isNode } from 'ipfs-utils/src/env.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from './utils/mocha.js'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import last from 'it-last'
import * as raw from 'multiformats/codecs/raw'
import * as dagPB from '@ipld/dag-pb'
import { sha256, sha512 } from 'multiformats/hashes/sha2'
const echoUrl = (/** @type {string} */ text) => `${process.env.ECHO_SERVER}/download?data=${encodeURIComponent(text)}`
const redirectUrl = (/** @type {string} */ url) => `${process.env.ECHO_SERVER}/redirect?to=${encodeURI(url)}`
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
* @typedef {import('ipfs-unixfs').MtimeLike} MtimeLike
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testAdd (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.add', function () {
this.timeout(1080 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/**
* @param {string | number} mode
* @param {number} expectedMode
*/
async function testMode (mode, expectedMode) {
const content = String(Math.random() + Date.now())
const file = await ipfs.add({
content,
mode
})
expect(file).to.have.property('mode', expectedMode)
const stats = await ipfs.files.stat(`/ipfs/${file.cid}`)
expect(stats).to.have.property('mode', expectedMode)
}
/**
* @param {MtimeLike} mtime
* @param {MtimeLike} expectedMtime
*/
async function testMtime (mtime, expectedMtime) {
const content = String(Math.random() + Date.now())
const file = await ipfs.add({
content,
mtime
})
expect(file).to.have.deep.property('mtime', expectedMtime)
const stats = await ipfs.files.stat(`/ipfs/${file.cid}`)
expect(stats).to.have.deep.property('mtime', expectedMtime)
}
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('should add a File', async function () {
if (!supportsFileReader) {
return this.skip()
}
const fileAdded = await ipfs.add(new File(['should add a File'], 'filename.txt', { type: 'text/plain' }))
expect(fileAdded.cid.toString()).to.be.eq('QmTVfLxf3qXiJgr4KwG6UBckcNvTqBp93Rwy5f7h3mHsVC')
})
it('should add a File as tuple', async function () {
if (!supportsFileReader) {
return this.skip()
}
const tuple = {
path: 'filename.txt',
content: new self.File(['should add a File'], 'filename.txt', { type: 'text/plain' })
}
const fileAdded = await ipfs.add(tuple)
expect(fileAdded.cid.toString()).to.be.eq('QmTVfLxf3qXiJgr4KwG6UBckcNvTqBp93Rwy5f7h3mHsVC')
})
it('should add a Uint8Array', async () => {
const file = await ipfs.add(fixtures.smallFile.data)
expect(file.cid.toString()).to.equal(fixtures.smallFile.cid.toString())
expect(file.path).to.equal(fixtures.smallFile.cid.toString())
// file.size counts the overhead by IPLD nodes and unixfs protobuf
expect(file.size).greaterThan(fixtures.smallFile.data.length)
})
it('should add a BIG Uint8Array', async () => {
const file = await ipfs.add(fixtures.bigFile.data)
expect(file.cid.toString()).to.equal(fixtures.bigFile.cid.toString())
expect(file.path).to.equal(fixtures.bigFile.cid.toString())
// file.size counts the overhead by IPLD nodes and unixfs protobuf
expect(file.size).greaterThan(fixtures.bigFile.data.length)
})
it('should add a BIG Uint8Array with progress enabled', async () => {
let progCalled = false
let accumProgress = 0
/**
* @type {import('ipfs-core-types/src/root').AddProgressFn}
*/
function handler (p) {
progCalled = true
accumProgress = p
}
const file = await ipfs.add(fixtures.bigFile.data, { progress: handler })
expect(file.cid.toString()).to.equal(fixtures.bigFile.cid.toString())
expect(file.path).to.equal(fixtures.bigFile.cid.toString())
expect(progCalled).to.be.true()
expect(accumProgress).to.equal(fixtures.bigFile.data.length)
})
it('should add an empty file with progress enabled', async () => {
let progCalled = false
let accumProgress = 0
/**
* @type {import('ipfs-core-types/src/root').AddProgressFn}
*/
function handler (p) {
progCalled = true
accumProgress = p
}
const file = await ipfs.add(fixtures.emptyFile.data, { progress: handler })
expect(file.cid.toString()).to.equal(fixtures.emptyFile.cid.toString())
expect(file.path).to.equal(fixtures.emptyFile.cid.toString())
expect(progCalled).to.be.true()
expect(accumProgress).to.equal(fixtures.emptyFile.data.length)
})
it('should receive file name from progress event', async () => {
let receivedName
/**
* @type {import('ipfs-core-types/src/root').AddProgressFn}
*/
function handler (p, name) {
receivedName = name
}
await ipfs.add({
content: 'hello',
path: 'foo.txt'
}, { progress: handler })
expect(receivedName).to.equal('foo.txt')
})
it('should add an empty file without progress enabled', async () => {
const file = await ipfs.add(fixtures.emptyFile.data)
expect(file.cid.toString()).to.equal(fixtures.emptyFile.cid.toString())
expect(file.path).to.equal(fixtures.emptyFile.cid.toString())
})
it('should add a Uint8Array as tuple', async () => {
const tuple = { path: 'testfile.txt', content: fixtures.smallFile.data }
const file = await ipfs.add(tuple)
expect(file.cid.toString()).to.equal(fixtures.smallFile.cid.toString())
expect(file.path).to.equal('testfile.txt')
})
it('should add a string', async () => {
const data = 'a string'
const expectedCid = 'QmQFRCwEpwQZ5aQMqCsCaFbdjNLLHoyZYDjr92v1F7HeqX'
const file = await ipfs.add(data)
expect(file).to.have.property('path', expectedCid)
expect(file).to.have.property('size', 16)
expect(`${file.cid}`).to.equal(expectedCid)
})
it('should add a TypedArray', async () => {
const data = Uint8Array.from([1, 3, 8])
const expectedCid = 'QmRyUEkVCuHC8eKNNJS9BDM9jqorUvnQJK1DM81hfngFqd'
const file = await ipfs.add(data)
expect(file).to.have.property('path', expectedCid)
expect(file).to.have.property('size', 11)
expect(`${file.cid}`).to.equal(expectedCid)
})
it('should add readable stream', async function () {
if (!isNode) {
this.skip()
}
const expectedCid = 'QmVv4Wz46JaZJeH5PMV4LGbRiiMKEmszPYY3g6fjGnVXBS'
const rs = new Readable()
rs.push(uint8ArrayFromString('some data'))
rs.push(null)
const file = await ipfs.add(rs)
expect(file).to.have.property('path', expectedCid)
expect(file).to.have.property('size', 17)
expect(`${file.cid}`).to.equal(expectedCid)
})
it('should fail when passed invalid input', async () => {
const nonValid = 138
// @ts-expect-error nonValid is non valid
await expect(ipfs.add(nonValid)).to.eventually.be.rejected()
})
it('should fail when passed undefined input', async () => {
// @ts-expect-error undefined is non valid
await expect(ipfs.add(undefined)).to.eventually.be.rejected()
})
it('should fail when passed null input', async () => {
// @ts-expect-error null is non valid
await expect(ipfs.add(null)).to.eventually.be.rejected()
})
it('should fail when passed multiple file objects', async () => {
const nonValid = [{ content: 'hello' }, { content: 'world' }]
// @ts-expect-error nonValid is non valid
await expect(ipfs.add(nonValid)).to.eventually.be.rejectedWith(/multiple items passed/)
})
it('should wrap content in a directory', async () => {
const data = { path: 'testfile.txt', content: fixtures.smallFile.data }
const wrapper = await ipfs.add(data, { wrapWithDirectory: true })
expect(wrapper.path).to.equal('')
const stats = await ipfs.files.stat(`/ipfs/${wrapper.cid}/testfile.txt`)
expect(`${stats.cid}`).to.equal(fixtures.smallFile.cid.toString())
})
it('should add with only-hash=true', async function () {
this.slow(10 * 1000)
const content = String(Math.random() + Date.now())
const file = await ipfs.add(content, { onlyHash: true })
await expect(ipfs.object.get(file.cid, { timeout: 4000 }))
.to.eventually.be.rejected()
.and.to.have.property('name').that.equals('TimeoutError')
})
it('should add with sha2-256 by default', async function () {
const content = String(Math.random() + Date.now())
const file = await ipfs.add(content)
expect(file).to.have.nested.property('cid.multihash.code', sha256.code)
})
it('should add with a different hashing algorithm', async function () {
const content = String(Math.random() + Date.now())
const file = await ipfs.add(content, { hashAlg: 'sha2-512' })
expect(file).to.have.nested.property('cid.multihash.code', sha512.code)
})
it('should add with mode as string', async function () {
this.slow(10 * 1000)
const mode = '0777'
await testMode(mode, parseInt(mode, 8))
})
it('should add with mode as number', async function () {
this.slow(10 * 1000)
const mode = parseInt('0777', 8)
await testMode(mode, mode)
})
it('should add with mtime as Date', async function () {
this.slow(10 * 1000)
const mtime = new Date(5000)
await testMtime(mtime, {
secs: 5,
nsecs: 0
})
})
it('should add with mtime as { nsecs, secs }', async function () {
this.slow(10 * 1000)
const mtime = {
secs: 5,
nsecs: 0
}
await testMtime(mtime, mtime)
})
it('should add with mtime as timespec', async function () {
this.slow(10 * 1000)
await testMtime({
Seconds: 5,
FractionalNanoseconds: 0
}, {
secs: 5,
nsecs: 0
})
})
it('should add with mtime as hrtime', async function () {
this.slow(10 * 1000)
const mtime = process.hrtime()
await testMtime(mtime, {
secs: mtime[0],
nsecs: mtime[1]
})
})
it('should add from a HTTP URL', async () => {
const text = `TEST${Math.random()}`
const url = echoUrl(text)
const [result, expectedResult] = await Promise.all([
ipfs.add(urlSource(url)),
ipfs.add(text)
])
expect(result.cid.toString()).to.equal(expectedResult.cid.toString())
expect(result.size).to.equal(expectedResult.size)
})
it('should add from a HTTP URL with redirection', async () => {
const text = `TEST${Math.random()}`
const url = echoUrl(text)
const [result, expectedResult] = await Promise.all([
ipfs.add(urlSource(redirectUrl(url))),
ipfs.add(text)
])
expect(result.cid.toString()).to.equal(expectedResult.cid.toString())
expect(result.size).to.equal(expectedResult.size)
})
it('should add from a URL with only-hash=true', async function () {
const text = `TEST${Math.random()}`
const url = echoUrl(text)
const res = await ipfs.add(urlSource(url), { onlyHash: true })
await expect(ipfs.object.get(res.cid, { timeout: 500 }))
.to.eventually.be.rejected()
.and.to.have.property('name').that.equals('TimeoutError')
})
it('should add from a URL with wrap-with-directory=true', async () => {
const filename = `TEST${Date.now()}.txt` // also acts as data
const url = echoUrl(filename)
const addOpts = { wrapWithDirectory: true }
const [result, expectedResult] = await Promise.all([
ipfs.add(urlSource(url), addOpts),
ipfs.add({ path: 'download', content: filename }, addOpts)
])
expect(result).to.deep.equal(expectedResult)
})
it('should add from a URL with wrap-with-directory=true and URL-escaped file name', async () => {
const filename = `320px-Domažlice,_Jiráskova_43_(${Date.now()}).jpg` // also acts as data
const url = echoUrl(filename)
const addOpts = { wrapWithDirectory: true }
const [result, expectedResult] = await Promise.all([
ipfs.add(urlSource(url), addOpts),
ipfs.add({ path: 'download', content: filename }, addOpts)
])
expect(result).to.deep.equal(expectedResult)
})
it('should not add from an invalid url', () => {
return expect(() => ipfs.add(urlSource('123http://invalid'))).to.throw()
})
it('should respect raw leaves when file is smaller than one block and no metadata is present', async () => {
const file = await ipfs.add(Uint8Array.from([0, 1, 2]), {
cidVersion: 1,
rawLeaves: true
})
expect(file.cid.toString()).to.equal('bafkreifojmzibzlof6xyh5auu3r5vpu5l67brf3fitaf73isdlglqw2t7q')
expect(file.cid.code).to.equal(raw.code)
expect(file.size).to.equal(3)
})
it('should override raw leaves when file is smaller than one block and metadata is present', async () => {
const file = await ipfs.add({
content: Uint8Array.from([0, 1, 2]),
mode: 0o123,
mtime: {
secs: 1000,
nsecs: 0
}
}, {
cidVersion: 1,
rawLeaves: true
})
expect(file.cid.toString()).to.equal('bafybeifmayxiu375ftlgydntjtffy5cssptjvxqw6vyuvtymntm37mpvua')
expect(file.cid.code).to.equal(dagPB.code)
expect(file.size).to.equal(18)
})
it('should add a file with a v1 CID', async () => {
const file = await ipfs.add(Uint8Array.from([0, 1, 2]), {
cidVersion: 1
})
expect(file.cid.toString()).to.equal('bafkreifojmzibzlof6xyh5auu3r5vpu5l67brf3fitaf73isdlglqw2t7q')
expect(file.size).to.equal(3)
})
const testFiles = Array.from(Array(1005), (_, i) => ({
path: 'test-folder/' + i,
content: uint8ArrayFromString('some content ' + i)
}))
it('should be able to add dir without sharding', async () => {
const result = await last(ipfs.addAll(testFiles))
if (!result) {
throw new Error('No addAll result received')
}
const { path, cid } = result
expect(path).to.eql('test-folder')
expect(cid.toString()).to.eql('QmWWM8ZV6GPhqJ46WtKcUaBPNHN5yQaFsKDSQ1RE73w94Q')
})
describe('with sharding', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async function () {
const ipfsd = await factory.spawn({
ipfsOptions: {
EXPERIMENTAL: {
// enable sharding for js
sharding: true
},
config: {
// enable sharding for go with automatic threshold dropped to the minimum so it shards everything
Internal: {
UnixFSShardingSizeThreshold: '1B'
}
}
}
})
ipfs = ipfsd.api
})
it('should be able to add dir with sharding', async () => {
const result = await last(ipfs.addAll(testFiles))
if (!result) {
throw new Error('No addAll result received')
}
const { path, cid } = result
expect(path).to.eql('test-folder')
expect(cid.toString()).to.eql('Qmb3JNLq2KcvDTSGT23qNQkMrr4Y4fYMktHh6DtC7YatLa')
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/bitswap/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testStat } from './stat.js'
import { testWantlist } from './wantlist.js'
import { testWantlistForPeer } from './wantlist-for-peer.js'
import { testTransfer } from './transfer.js'
import { testUnwant } from './unwant.js'
const tests = {
stat: testStat,
wantlist: testWantlist,
wantlistForPeer: testWantlistForPeer,
transfer: testTransfer,
unwant: testUnwant
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/bitswap/stat.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { expectIsBitswap } from '../stats/utils.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testStat (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.bitswap.stat', function () {
this.timeout(60 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get bitswap stats', async () => {
const res = await ipfs.bitswap.stat()
expectIsBitswap(null, res)
})
it('should not get bitswap stats when offline', async () => {
const node = await factory.spawn()
await node.stop()
return expect(node.api.bitswap.stat()).to.eventually.be.rejected()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/bitswap/transfer.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { isWebWorker } from 'ipfs-utils/src/env.js'
import { randomBytes } from 'iso-random-stream'
import concat from 'it-concat'
import { nanoid } from 'nanoid'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import pmap from 'p-map'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
* @typedef {import('multiformats').CID} CID
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testTransfer (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('transfer blocks', function () {
this.timeout(540 * 1000)
afterEach(() => factory.clean())
describe('transfer a block between', () => {
it('2 peers', async function () {
// webworkers are not dialable because webrtc is not available
const remote = (await factory.spawn({ type: isWebWorker ? 'go' : undefined })).api
const remoteId = await remote.id()
const local = (await factory.spawn({ type: 'proc' })).api
await local.swarm.connect(remoteId.addresses[0])
const data = uint8ArrayFromString(`IPFS is awesome ${nanoid()}`)
const cid = await local.block.put(data)
const b = await remote.block.get(cid)
expect(b).to.equalBytes(data)
})
it('3 peers', async () => {
const blocks = Array(6).fill(0).map(() => uint8ArrayFromString(`IPFS is awesome ${nanoid()}`))
const remote1 = (await factory.spawn({ type: isWebWorker ? 'go' : undefined })).api
const remote1Id = await remote1.id()
const remote2 = (await factory.spawn({ type: isWebWorker ? 'go' : undefined })).api
const remote2Id = await remote2.id()
const local = (await factory.spawn({ type: 'proc' })).api
await local.swarm.connect(remote1Id.addresses[0])
await local.swarm.connect(remote2Id.addresses[0])
await remote1.swarm.connect(remote2Id.addresses[0])
// order is important
/** @type {CID[]} */
const cids = []
cids.push(await remote1.block.put(blocks[0]))
cids.push(await remote1.block.put(blocks[1]))
cids.push(await remote2.block.put(blocks[2]))
cids.push(await remote2.block.put(blocks[3]))
cids.push(await local.block.put(blocks[4]))
cids.push(await local.block.put(blocks[5]))
await pmap(blocks, async (block, i) => {
expect(await remote1.block.get(cids[i])).to.eql(block)
expect(await remote2.block.get(cids[i])).to.eql(block)
expect(await local.block.get(cids[i])).to.eql(block)
}, { concurrency: 3 })
})
})
describe('transfer a file between', () => {
it('2 peers', async () => {
const content = randomBytes(1024)
const remote = (await factory.spawn({ type: isWebWorker ? 'go' : undefined })).api
const remoteId = await remote.id()
const local = (await factory.spawn({ type: 'proc' })).api
local.swarm.connect(remoteId.addresses[0])
const file = await remote.add({ path: 'awesome.txt', content })
const data = await concat(local.cat(file.cid))
expect(data.slice()).to.eql(content)
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/bitswap/unwant.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testUnwant (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.bitswap.unwant', function () {
this.timeout(60 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should throw error for invalid CID input', async () => {
// @ts-expect-error input is invalid
await expect(ipfs.bitswap.unwant('INVALID CID')).to.eventually.be.rejected()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/bitswap/utils.js
================================================
import delay from 'delay'
/**
* @typedef {import('@libp2p/interface-peer-id').PeerId} PeerId
*/
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {string} key
* @param {{ timeout?: number, interval?: number, peerId?: PeerId }} [opts]
*/
export async function waitForWantlistKey (ipfs, key, opts = {}) {
opts.timeout = opts.timeout || 10000
opts.interval = opts.interval || 100
const end = Date.now() + opts.timeout
while (Date.now() < end) {
let list
if (opts.peerId) {
list = await ipfs.bitswap.wantlistForPeer(opts.peerId)
} else {
list = await ipfs.bitswap.wantlist()
}
if (list.some(cid => cid.toString() === key)) {
return
}
await delay(opts.interval)
}
throw new Error(`Timed out waiting for ${key} in wantlist`)
}
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {string} key
* @param {{ timeout?: number, interval?: number, peerId?: PeerId }} [opts]
*/
export async function waitForWantlistKeyToBeRemoved (ipfs, key, opts = {}) {
opts.timeout = opts.timeout || 10000
opts.interval = opts.interval || 100
const end = Date.now() + opts.timeout
while (Date.now() < end) {
let list
if (opts.peerId) {
list = await ipfs.bitswap.wantlistForPeer(opts.peerId)
} else {
list = await ipfs.bitswap.wantlist()
}
if (list.some(cid => cid.toString() === key)) {
await delay(opts.interval)
continue
}
return
}
throw new Error(`Timed out waiting for ${key} to be removed from wantlist`)
}
================================================
FILE: packages/interface-ipfs-core/src/bitswap/wantlist-for-peer.js
================================================
/* eslint-env mocha */
import { getDescribe, getIt } from '../utils/mocha.js'
import { waitForWantlistKey } from './utils.js'
import { isWebWorker } from 'ipfs-utils/src/env.js'
import { CID } from 'multiformats/cid'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testWantlistForPeer (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.bitswap.wantlistForPeer', function () {
this.timeout(60 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfsA
/** @type {import('ipfs-core-types').IPFS} */
let ipfsB
const key = 'QmUBdnXXPyoDFXj3Hj39dNJ5VkN3QFRskXxcGaYFBB8CNR'
before(async () => {
ipfsA = (await factory.spawn({ type: 'proc' })).api
// webworkers are not dialable because webrtc is not available
ipfsB = (await factory.spawn({ type: isWebWorker ? 'go' : undefined })).api
// Add key to the wantlist for ipfsB
ipfsB.block.get(CID.parse(key)).catch(() => { /* is ok, expected on teardown */ })
const ipfsBId = await ipfsB.id()
await ipfsA.swarm.connect(ipfsBId.addresses[0])
})
after(() => factory.clean())
it('should get the wantlist by peer ID for a different node', async () => {
const ipfsBId = await ipfsB.id()
return waitForWantlistKey(ipfsA, key, {
peerId: ipfsBId.id,
timeout: 60 * 1000
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/bitswap/wantlist.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { waitForWantlistKey, waitForWantlistKeyToBeRemoved } from './utils.js'
import { isWebWorker } from 'ipfs-utils/src/env.js'
import testTimeout from '../utils/test-timeout.js'
import { CID } from 'multiformats/cid'
import delay from 'delay'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testWantlist (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.bitswap.wantlist', function () {
this.timeout(60 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfsA
/** @type {import('ipfs-core-types').IPFS} */
let ipfsB
const key = 'QmUBdnXXPyoDFXj3Hj39dNJ5VkN3QFRskXxcGaYFBB8CNR'
before(async () => {
ipfsA = (await factory.spawn({ type: 'proc' })).api
// webworkers are not dialable because webrtc is not available
ipfsB = (await factory.spawn({ type: isWebWorker ? 'go' : undefined })).api
// Add key to the wantlist for ipfsB
ipfsB.block.get(CID.parse(key)).catch(() => { /* is ok, expected on teardown */ })
const ipfsBId = await ipfsB.id()
await ipfsA.swarm.connect(ipfsBId.addresses[0])
})
after(() => factory.clean())
it('should respect timeout option when getting bitswap wantlist', () => {
return testTimeout(() => ipfsA.bitswap.wantlist({
timeout: 1
}))
})
it('should get the wantlist', function () {
return waitForWantlistKey(ipfsB, key)
})
it('should not get the wantlist when offline', async () => {
const node = await factory.spawn()
await node.stop()
return expect(node.api.bitswap.stat()).to.eventually.be.rejected()
})
it('should remove blocks from the wantlist when requests are cancelled', async () => {
const controller = new AbortController()
const cid = CID.parse('QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KaGa')
const getPromise = ipfsA.dag.get(cid, {
signal: controller.signal
})
await waitForWantlistKey(ipfsA, cid.toString())
controller.abort()
await expect(getPromise).to.eventually.be.rejectedWith(/aborted/)
await waitForWantlistKeyToBeRemoved(ipfsA, cid.toString())
})
it('should keep blocks in the wantlist when only one request is cancelled', async () => {
const controller = new AbortController()
const otherController = new AbortController()
const cid = CID.parse('QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1Kaaa')
const getPromise = ipfsA.dag.get(cid, {
signal: controller.signal
})
const otherGetPromise = ipfsA.dag.get(cid, {
signal: otherController.signal
})
await waitForWantlistKey(ipfsA, cid.toString())
controller.abort()
await expect(getPromise).to.eventually.be.rejectedWith(/aborted/)
await delay(1000)
// cid should still be in the wantlist
await waitForWantlistKey(ipfsA, cid.toString())
otherController.abort()
await expect(otherGetPromise).to.eventually.be.rejectedWith(/aborted/)
await waitForWantlistKeyToBeRemoved(ipfsA, cid.toString())
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/block/get.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { identity } from 'multiformats/hashes/identity'
import { CID } from 'multiformats/cid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import testTimeout from '../utils/test-timeout.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testGet (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.block.get', () => {
const data = uint8ArrayFromString('blorb')
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/** @type {CID} */
let cid
before(async () => {
ipfs = (await factory.spawn()).api
cid = await ipfs.block.put(data)
})
after(() => factory.clean())
it('should respect timeout option when getting a block', () => {
return testTimeout(() => ipfs.block.get(CID.parse('QmPv52ekjS75L4JmHpXVeuJ5uX2ecSfSZo88NSyxwA3rA3'), {
timeout: 1
}))
})
it('should get by CID', async () => {
const block = await ipfs.block.get(cid)
expect(block).to.equalBytes(uint8ArrayFromString('blorb'))
})
it('should get an empty block', async () => {
const cid = await ipfs.block.put(new Uint8Array(0), {
format: 'dag-pb',
mhtype: 'sha2-256',
version: 0
})
const block = await ipfs.block.get(cid)
expect(block).to.equalBytes(new Uint8Array(0))
})
it('should get a block added as CIDv0 with a CIDv1', async () => {
const input = uint8ArrayFromString(`TEST${Math.random()}`)
const cidv0 = await ipfs.block.put(input)
expect(cidv0.version).to.equal(0)
const cidv1 = cidv0.toV1()
const block = await ipfs.block.get(cidv1)
expect(block).to.equalBytes(input)
})
it('should get a block added as CIDv1 with a CIDv0', async () => {
const input = uint8ArrayFromString(`TEST${Math.random()}`)
const cidv1 = await ipfs.block.put(input, {
version: 1,
format: 'dag-pb'
})
expect(cidv1.version).to.equal(1)
const cidv0 = cidv1.toV0()
const block = await ipfs.block.get(cidv0)
expect(block).to.equalBytes(input)
})
it('should get a block with an identity CID, without putting first', async () => {
const identityData = uint8ArrayFromString('A16461736466190144', 'base16upper')
const identityHash = await identity.digest(identityData)
const identityCID = CID.createV1(identity.code, identityHash)
const block = await ipfs.block.get(identityCID)
expect(block).to.equalBytes(identityData)
})
it('should return an error for an invalid CID', () => {
// @ts-expect-error invalid input
return expect(ipfs.block.get('Non-base58 character')).to.eventually.be.rejected
.and.be.an.instanceOf(Error)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/block/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testGet } from './get.js'
import { testPut } from './put.js'
import { testRm } from './rm.js'
import { testStat } from './stat.js'
const tests = {
get: testGet,
put: testPut,
rm: testRm,
stat: testStat
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/block/put.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { base58btc } from 'multiformats/bases/base58'
import { CID } from 'multiformats/cid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import all from 'it-all'
import * as raw from 'multiformats/codecs/raw'
import { sha512 } from 'multiformats/hashes/sha2'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testPut (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.block.put', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should put a buffer, using defaults', async () => {
const expectedHash = 'QmPv52ekjS75L4JmHpXVeuJ5uX2ecSfSZo88NSyxwA3rAQ'
const blob = uint8ArrayFromString('blorb')
const cid = await ipfs.block.put(blob)
expect(cid.toString()).to.equal(expectedHash)
expect(cid.bytes).to.equalBytes(base58btc.decode(`z${expectedHash}`))
})
it('should put a buffer, using options', async () => {
const blob = uint8ArrayFromString(`TEST${Math.random()}`)
const cid = await ipfs.block.put(blob, {
format: 'raw',
mhtype: 'sha2-512',
version: 1,
pin: true
})
expect(cid.version).to.equal(1)
expect(cid.code).to.equal(raw.code)
expect(cid.multihash.code).to.equal(sha512.code)
expect(await all(ipfs.pin.ls({ paths: cid }))).to.have.lengthOf(1)
})
it('should put a Block instance', async () => {
const expectedHash = 'QmPv52ekjS75L4JmHpXVeuJ5uX2ecSfSZo88NSyxwA3rAQ'
const expectedCID = CID.parse(expectedHash)
const b = uint8ArrayFromString('blorb')
const cid = await ipfs.block.put(b)
expect(cid.multihash.bytes).to.equalBytes(expectedCID.multihash.bytes)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/block/rm.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { nanoid } from 'nanoid'
import all from 'it-all'
import last from 'it-last'
import drain from 'it-drain'
import { CID } from 'multiformats/cid'
import * as raw from 'multiformats/codecs/raw'
import testTimeout from '../utils/test-timeout.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testRm (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.block.rm', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('should respect timeout option when removing a block', () => {
return testTimeout(() => drain(ipfs.block.rm(CID.parse('QmVwdDCY4SPGVFnNCiZnX5CtzwWDn6kAM98JXzKxE3kCmn'), {
timeout: 1
})))
})
it('should remove by CID object', async () => {
const cid = await ipfs.dag.put(uint8ArrayFromString(nanoid()), {
storeCodec: 'raw',
hashAlg: 'sha2-256'
})
// block should be present in the local store
const localRefs = await all(ipfs.refs.local())
expect(localRefs).to.have.property('length').that.is.greaterThan(0)
expect(localRefs.find(ref => ref.ref === CID.createV1(raw.code, cid.multihash).toString())).to.be.ok()
const result = await all(ipfs.block.rm(cid))
expect(result).to.be.an('array').and.to.have.lengthOf(1)
expect(result[0].cid.toString()).equal(cid.toString())
expect(result[0]).to.not.have.property('error')
// did we actually remove the block?
const localRefsAfterRemove = await all(ipfs.refs.local())
expect(localRefsAfterRemove.find(ref => ref.ref === CID.createV1(raw.code, cid.multihash).toString())).to.not.be.ok()
})
it('should remove multiple CIDs', async () => {
const cids = await Promise.all([
ipfs.dag.put(uint8ArrayFromString(nanoid()), {
storeCodec: 'raw',
hashAlg: 'sha2-256'
}),
ipfs.dag.put(uint8ArrayFromString(nanoid()), {
storeCodec: 'raw',
hashAlg: 'sha2-256'
}),
ipfs.dag.put(uint8ArrayFromString(nanoid()), {
storeCodec: 'raw',
hashAlg: 'sha2-256'
})
])
const result = await all(ipfs.block.rm(cids))
expect(result).to.have.lengthOf(3)
result.forEach((res) => {
expect(cids.map(cid => cid.toString())).to.include(res.cid.toString())
expect(res).to.not.have.property('error')
})
})
it('should error when removing non-existent blocks', async () => {
const cid = await ipfs.dag.put(uint8ArrayFromString(nanoid()), {
storeCodec: 'raw',
hashAlg: 'sha2-256'
})
// remove it
await all(ipfs.block.rm(cid))
// remove it again
const result = await all(ipfs.block.rm(cid))
expect(result).to.be.an('array').and.to.have.lengthOf(1)
expect(result).to.have.nested.property('[0].error.message').that.includes('block not found')
})
it('should not error when force removing non-existent blocks', async () => {
const cid = await ipfs.dag.put(uint8ArrayFromString(nanoid()), {
storeCodec: 'raw',
hashAlg: 'sha2-256'
})
// remove it
await all(ipfs.block.rm(cid))
// remove it again
const result = await all(ipfs.block.rm(cid, { force: true }))
expect(result).to.be.an('array').and.to.have.lengthOf(1)
expect(result[0].cid.toString()).to.equal(cid.toString())
expect(result[0]).to.not.have.property('error')
})
it('should return empty output when removing blocks quietly', async () => {
const cid = await ipfs.dag.put(uint8ArrayFromString(nanoid()), {
storeCodec: 'raw',
hashAlg: 'sha2-256'
})
const result = await all(ipfs.block.rm(cid, { quiet: true }))
expect(result).to.be.an('array').and.to.have.lengthOf(0)
})
it('should error when removing pinned blocks', async () => {
const cid = await ipfs.dag.put(uint8ArrayFromString(nanoid()), {
storeCodec: 'raw',
hashAlg: 'sha2-256'
})
await ipfs.pin.add(cid)
const result = await last(ipfs.block.rm(cid))
expect(result).to.have.property('error').that.is.an('Error')
.with.property('message').that.includes('pinned')
})
it('should throw error for invalid CID input', () => {
// @ts-expect-error invalid input
return expect(all(ipfs.block.rm('INVALID CID')))
.to.eventually.be.rejected()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/block/stat.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { CID } from 'multiformats/cid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import testTimeout from '../utils/test-timeout.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testStat (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.block.stat', () => {
const data = uint8ArrayFromString('blorb')
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/** @type {CID} */
let cid
before(async () => {
ipfs = (await factory.spawn()).api
cid = await ipfs.block.put(data)
})
after(() => factory.clean())
it('should respect timeout option when statting a block', () => {
return testTimeout(() => ipfs.block.stat(CID.parse('QmVwdDCY4SPGVFnNCiZnX5CtzwWDn6kAM98JXzKxE3kCmn'), {
timeout: 1
}))
})
it('should stat by CID', async () => {
const stats = await ipfs.block.stat(cid)
expect(stats.cid.toString()).to.equal(cid.toString())
expect(stats).to.have.property('size', data.length)
})
it('should return error for missing argument', () => {
// @ts-expect-error invalid input
return expect(ipfs.block.stat(null)).to.eventually.be.rejected
.and.be.an.instanceOf(Error)
})
it('should return error for invalid argument', () => {
// @ts-expect-error invalid input
return expect(ipfs.block.stat('invalid')).to.eventually.be.rejected
.and.be.an.instanceOf(Error)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/bootstrap/add.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { multiaddr, isMultiaddr } from '@multiformats/multiaddr'
const invalidArg = 'this/Is/So/Invalid/'
const validIp4 = multiaddr('/ip4/104.236.176.52/tcp/4001/p2p/QmSoLnSGccFuZQJzRadHn95W2CrSFmZuTdDWP8HXaHca9z')
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testAdd (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.bootstrap.add', function () {
this.timeout(100 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should return an error when called with an invalid arg', () => {
// @ts-expect-error invalid input
return expect(ipfs.bootstrap.add(invalidArg)).to.eventually.be.rejected
.and.be.an.instanceOf(Error)
})
it('should return a list containing the bootstrap peer when called with a valid arg (ip4)', async () => {
const res = await ipfs.bootstrap.add(validIp4)
expect(res).to.be.eql({ Peers: [validIp4] })
const peers = res.Peers
expect(peers).to.have.property('length').that.is.equal(1)
})
it('should prevent duplicate inserts of bootstrap peers', async () => {
await ipfs.bootstrap.clear()
const added = await ipfs.bootstrap.add(validIp4)
expect(added).to.have.property('Peers').that.deep.equals([validIp4])
const addedAgain = await ipfs.bootstrap.add(validIp4)
expect(addedAgain).to.have.property('Peers').that.deep.equals([validIp4])
const list = await ipfs.bootstrap.list()
expect(list).to.have.property('Peers').that.deep.equals([validIp4])
})
it('add a peer to the bootstrap list', async () => {
const peer = multiaddr('/ip4/111.111.111.111/tcp/1001/p2p/QmXFX2P5ammdmXQgfqGkfswtEVFsZUJ5KeHRXQYCTdiTAb')
const res = await ipfs.bootstrap.add(peer)
expect(res).to.be.eql({ Peers: [peer] })
const list = await ipfs.bootstrap.list()
expect(list.Peers).to.deep.include(peer)
expect(list.Peers.every(ma => isMultiaddr(ma))).to.be.true()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/bootstrap/clear.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { multiaddr, isMultiaddr } from '@multiformats/multiaddr'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testClear (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
const validIp4 = multiaddr('/ip4/104.236.176.52/tcp/4001/p2p/QmSoLnSGccFuZQJzRadHn95W2CrSFmZuTdDWP8HXaHca9z')
describe('.bootstrap.clear', function () {
this.timeout(100 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('should return a list containing the peer removed when called with a valid arg (ip4)', async () => {
await ipfs.bootstrap.clear()
const addRes = await ipfs.bootstrap.add(validIp4)
expect(addRes).to.be.eql({ Peers: [validIp4] })
const rmRes = await ipfs.bootstrap.clear()
expect(rmRes).to.be.eql({ Peers: [validIp4] })
const peers = rmRes.Peers
expect(peers).to.have.property('length').that.is.equal(1)
})
it('should return a list of all peers removed when all option is passed', async () => {
const addRes = await ipfs.bootstrap.reset()
const addedPeers = addRes.Peers
const rmRes = await ipfs.bootstrap.clear()
const removedPeers = rmRes.Peers
expect(removedPeers.sort()).to.deep.equal(addedPeers.sort())
expect(removedPeers.every(ma => isMultiaddr(ma))).to.be.true()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/bootstrap/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testAdd } from './add.js'
import { testClear } from './clear.js'
import { testList } from './list.js'
import { testReset } from './reset.js'
import { testRm } from './rm.js'
const tests = {
add: testAdd,
clear: testClear,
list: testList,
reset: testReset,
rm: testRm
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/bootstrap/list.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { isMultiaddr } from '@multiformats/multiaddr'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testList (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.bootstrap.list', function () {
this.timeout(100 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('should return a list of peers', async () => {
const res = await ipfs.bootstrap.list()
const peers = res.Peers
expect(peers).to.be.an('Array')
expect(peers.every(ma => isMultiaddr(ma))).to.be.true()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/bootstrap/reset.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { isMultiaddr } from '@multiformats/multiaddr'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testReset (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.bootstrap.reset', function () {
this.timeout(100 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should return a list of bootstrap peers when resetting the bootstrap nodes', async () => {
const res = await ipfs.bootstrap.reset()
const peers = res.Peers
expect(peers).to.have.property('length').that.is.gt(1)
})
it('should return a list of all peers removed when all option is passed', async () => {
const addRes = await ipfs.bootstrap.reset()
const addedPeers = addRes.Peers
const rmRes = await ipfs.bootstrap.clear()
const removedPeers = rmRes.Peers
expect(removedPeers.sort()).to.deep.equal(addedPeers.sort())
expect(addedPeers.every(ma => isMultiaddr(ma))).to.be.true()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/bootstrap/rm.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { multiaddr, isMultiaddr } from '@multiformats/multiaddr'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testRm (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
const invalidArg = 'this/Is/So/Invalid/'
const validIp4 = multiaddr('/ip4/104.236.176.52/tcp/4001/p2p/QmSoLnSGccFuZQJzRadHn95W2CrSFmZuTdDWP8HXaHca9z')
describe('.bootstrap.rm', function () {
this.timeout(100 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('should return an error when called with an invalid arg', () => {
// @ts-expect-error invalid input
return expect(ipfs.bootstrap.rm(invalidArg)).to.eventually.be.rejected
.and.be.an.instanceOf(Error)
})
it('should return a list containing the peer removed when called with a valid arg (ip4)', async () => {
const addRes = await ipfs.bootstrap.add(validIp4)
expect(addRes).to.be.eql({ Peers: [validIp4] })
const rmRes = await ipfs.bootstrap.rm(validIp4)
expect(rmRes).to.be.eql({ Peers: [validIp4] })
const peers = rmRes.Peers
expect(peers).to.have.property('length').that.is.equal(1)
})
it('removes a peer from the bootstrap list', async () => {
const peer = multiaddr('/ip4/111.111.111.111/tcp/1001/p2p/QmXFX2P5ammdmXQgfqGkfswtEVFsZUJ5KeHRXQYCTdiTAb')
await ipfs.bootstrap.add(peer)
let list = await ipfs.bootstrap.list()
expect(list.Peers).to.deep.include(peer)
const res = await ipfs.bootstrap.rm(peer)
expect(res).to.be.eql({ Peers: [peer] })
list = await ipfs.bootstrap.list()
expect(list.Peers).to.not.deep.include(peer)
expect(res.Peers.every(ma => isMultiaddr(ma))).to.be.true()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/cat.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import { fixtures } from './utils/index.js'
import { CID } from 'multiformats/cid'
import all from 'it-all'
import drain from 'it-drain'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from './utils/mocha.js'
import testTimeout from './utils/test-timeout.js'
import { importer } from 'ipfs-unixfs-importer'
import blockstore from './utils/blockstore-adapter.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testCat (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.cat', function () {
this.timeout(120 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
before(() => Promise.all([
all(importer({ content: fixtures.smallFile.data }, blockstore(ipfs))),
all(importer({ content: fixtures.bigFile.data }, blockstore(ipfs)))
]))
it('should respect timeout option when catting files', () => {
return testTimeout(() => drain(ipfs.cat(CID.parse('QmPDqvcuA4AkhBLBuh2y49yhUB98rCnxPxa3eVNC1kAbS1'), {
timeout: 1
})))
})
it('should cat with a base58 string encoded multihash', async () => {
const data = uint8ArrayConcat(await all(ipfs.cat(fixtures.smallFile.cid)))
expect(uint8ArrayToString(data)).to.contain('Plz add me!')
})
it('should cat with a Uint8Array multihash', async () => {
const cid = fixtures.smallFile.cid
const data = uint8ArrayConcat(await all(ipfs.cat(cid)))
expect(uint8ArrayToString(data)).to.contain('Plz add me!')
})
it('should cat with a CID object', async () => {
const cid = fixtures.smallFile.cid
const data = uint8ArrayConcat(await all(ipfs.cat(cid)))
expect(uint8ArrayToString(data)).to.contain('Plz add me!')
})
it('should cat a file added as CIDv0 with a CIDv1', async () => {
const input = uint8ArrayFromString(`TEST${Math.random()}`)
const res = await all(importer([{ content: (async function * () { yield input }()) }], blockstore(ipfs)))
expect(res).to.have.nested.property('[0].cid.version', 0)
const cidv1 = res[0].cid.toV1()
const output = uint8ArrayConcat(await all(ipfs.cat(cidv1)))
expect(output).to.eql(input)
})
it('should cat a file added as CIDv1 with a CIDv0', async () => {
const input = uint8ArrayFromString(`TEST${Math.random()}`)
const res = await all(importer([{ content: (async function * () { yield input }()) }], blockstore(ipfs), { cidVersion: 1, rawLeaves: false }))
expect(res).to.have.nested.property('[0].cid.version', 1)
const cidv0 = res[0].cid.toV0()
const output = uint8ArrayConcat(await all(ipfs.cat(cidv0)))
expect(output.slice()).to.eql(input)
})
it('should cat a BIG file', async () => {
const data = uint8ArrayConcat(await all(ipfs.cat(fixtures.bigFile.cid)))
expect(data.length).to.equal(fixtures.bigFile.data.length)
expect(data.slice()).to.eql(fixtures.bigFile.data)
})
it('should cat with IPFS path', async () => {
const ipfsPath = '/ipfs/' + fixtures.smallFile.cid
const data = uint8ArrayConcat(await all(ipfs.cat(ipfsPath)))
expect(uint8ArrayToString(data)).to.contain('Plz add me!')
})
it('should cat with IPFS path, nested value', async () => {
const fileToAdd = { path: 'a/testfile.txt', content: fixtures.smallFile.data }
const filesAdded = await all(importer(fileToAdd, blockstore(ipfs)))
const file = await filesAdded.find((f) => f.path === 'a')
expect(file).to.exist()
if (!file) {
throw new Error('No file added')
}
const data = uint8ArrayConcat(await all(ipfs.cat(`/ipfs/${file.cid}/testfile.txt`)))
expect(uint8ArrayToString(data)).to.contain('Plz add me!')
})
it('should cat with IPFS path, deeply nested value', async () => {
const fileToAdd = { path: 'a/b/testfile.txt', content: fixtures.smallFile.data }
const filesAdded = await all(importer([fileToAdd], blockstore(ipfs)))
const file = filesAdded.find((f) => f.path === 'a')
expect(file).to.exist()
if (!file) {
throw new Error('No file added')
}
const data = uint8ArrayConcat(await all(ipfs.cat(`/ipfs/${file.cid}/b/testfile.txt`)))
expect(uint8ArrayToString(data)).to.contain('Plz add me!')
})
it('should error on invalid key', () => {
const invalidCid = 'somethingNotMultihash'
return expect(drain(ipfs.cat(invalidCid))).to.eventually.be.rejected()
})
it('should error on unknown path', () => {
return expect(drain(ipfs.cat(fixtures.smallFile.cid + '/does-not-exist'))).to.eventually.be.rejected()
.and.be.an.instanceOf(Error)
.and.to.have.property('message')
.to.be.oneOf([
'file does not exist',
'no link named "does-not-exist" under Qma4hjFTnCasJ8PVp3mZbZK5g2vGDT4LByLJ7m8ciyRFZP'
])
})
it('should error on dir path', async () => {
const file = { path: 'dir/testfile.txt', content: fixtures.smallFile.data }
const filesAdded = await all(importer([file], blockstore(ipfs)))
expect(filesAdded.length).to.equal(2)
const files = filesAdded.filter((file) => file.path === 'dir')
expect(files.length).to.equal(1)
const dir = files[0]
const err = await expect(drain(ipfs.cat(dir.cid))).to.eventually.be.rejected()
expect(err.message).to.contain('this dag node is a directory')
})
it('should export a chunk of a file', async () => {
const offset = 1
const length = 3
const data = uint8ArrayConcat(await all(ipfs.cat(fixtures.smallFile.cid, { offset, length })))
expect(uint8ArrayToString(data)).to.equal('lz ')
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/config/get.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testGet (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.config.get', function () {
this.timeout(30 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('should fail with error', async () => {
// @ts-expect-error missing arg
await expect(ipfs.config.get()).to.eventually.rejectedWith('key argument is required')
})
it('should retrieve a value through a key', async () => {
const peerId = await ipfs.config.get('Identity.PeerID')
expect(peerId).to.exist()
})
it('should retrieve a value through a nested key', async () => {
const swarmAddrs = await ipfs.config.get('Addresses.Swarm')
expect(swarmAddrs).to.exist()
})
it('should fail on non valid key', () => {
// @ts-expect-error invalid arg
return expect(ipfs.config.get(1234)).to.eventually.be.rejected()
})
it('should fail on non existent key', () => {
return expect(ipfs.config.get('Bananas')).to.eventually.be.rejected()
})
})
describe('.config.getAll', function () {
this.timeout(30 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('should retrieve the whole config', async () => {
const config = await ipfs.config.getAll()
expect(config).to.be.an('object')
})
it('should retrieve the whole config with options', async () => {
const config = await ipfs.config.getAll({ signal: undefined })
expect(config).to.be.an('object')
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/config/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testGet } from './get.js'
import { testSet } from './set.js'
import { testReplace } from './replace.js'
import profiles from './profiles/index.js'
const tests = {
get: testGet,
set: testSet,
replace: testReplace,
profiles
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/config/profiles/apply.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testApply (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.config.profiles.apply', function () {
this.timeout(30 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should apply a config profile', async () => {
const diff = await ipfs.config.profiles.apply('lowpower')
expect(diff.original.Swarm?.ConnMgr?.LowWater).to.not.equal(diff.updated.Swarm?.ConnMgr?.LowWater)
const newConfig = await ipfs.config.getAll()
expect(newConfig.Swarm?.ConnMgr?.LowWater).to.equal(diff.updated.Swarm?.ConnMgr?.LowWater)
})
it('should strip private key from diff output', async () => {
const originalConfig = await ipfs.config.getAll()
const diff = await ipfs.config.profiles.apply('default-networking', { dryRun: true })
// should have stripped private key from diff output
expect(originalConfig).to.have.nested.property('Identity.PrivKey')
expect(diff).to.not.have.nested.property('original.Identity.PrivKey')
expect(diff).to.not.have.nested.property('updated.Identity.PrivKey')
})
it('should not apply a config profile in dry-run mode', async () => {
const originalConfig = await ipfs.config.getAll()
await ipfs.config.profiles.apply('server', { dryRun: true })
const updatedConfig = await ipfs.config.getAll()
expect(updatedConfig).to.deep.equal(originalConfig)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/config/profiles/index.js
================================================
import { createSuite } from '../../utils/suite.js'
import { testApply } from './apply.js'
import { testList } from './list.js'
const tests = {
apply: testApply,
list: testList
}
export default createSuite(tests, 'config')
================================================
FILE: packages/interface-ipfs-core/src/config/profiles/list.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testList (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.config.profiles.list', function () {
this.timeout(30 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should list config profiles', async () => {
const profiles = await ipfs.config.profiles.list()
expect(profiles).to.be.an('array')
expect(profiles).not.to.be.empty()
profiles.forEach(profile => {
expect(profile.name).to.be.a('string')
expect(profile.description).to.be.a('string')
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/config/replace.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testReplace (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.config.replace', function () {
this.timeout(30 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
const config = {
Addresses: {
API: ''
}
}
it('should replace the whole config', async () => {
await ipfs.config.replace(config)
const _config = await ipfs.config.getAll()
expect(_config).to.deep.equal(config)
})
it('should replace to empty config', async () => {
await ipfs.config.replace({})
const _config = await ipfs.config.getAll()
expect(_config).to.deep.equal({})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/config/set.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testSet (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.config.set', function () {
this.timeout(30 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should set a new key', async () => {
await ipfs.config.set('Fruit', 'banana')
const fruit = await ipfs.config.get('Fruit')
expect(fruit).to.equal('banana')
})
it('should set an already existing key', async () => {
await ipfs.config.set('Fruit', 'morango')
const fruit = await ipfs.config.get('Fruit')
expect(fruit).to.equal('morango')
})
it('should set a number', async () => {
const key = 'Discovery.MDNS.Interval'
const val = 11
await ipfs.config.set(key, val)
const result = await ipfs.config.get(key)
expect(result).to.equal(val)
})
it('should set a boolean', async () => {
const value = true
const key = 'Discovery.MDNS.Enabled'
await ipfs.config.set(key, value)
expect(await ipfs.config.get(key)).to.equal(value)
})
it('should set the other boolean', async () => {
const value = false
const key = 'Discovery.MDNS.Enabled'
await ipfs.config.set(key, value)
expect(await ipfs.config.get(key)).to.equal(value)
})
it('should set a JSON object', async () => {
const key = 'API.HTTPHeaders.Access-Control-Allow-Origin'
const val = ['http://example.io']
await ipfs.config.set(key, val)
const result = await ipfs.config.get(key)
expect(result).to.deep.equal(val)
})
it('should fail on non valid key', () => {
// @ts-expect-error invalid arg
return expect(ipfs.config.set(uint8ArrayFromString('heeey'), '')).to.eventually.be.rejected()
})
it('should fail on non valid value', () => {
const val = {}
val.val = val
return expect(ipfs.config.set('Fruit', val)).to.eventually.be.rejected()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/dag/export.js
================================================
/* eslint-env mocha */
import all from 'it-all'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { CarReader } from '@ipld/car'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import * as dagPB from '@ipld/dag-pb'
import * as dagCBOR from '@ipld/dag-cbor'
import loadFixture from 'aegir/fixtures'
import toBuffer from 'it-to-buffer'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testExport (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.dag.export', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should export a car file', async () => {
const child = dagPB.encode({
Data: uint8ArrayFromString('block-' + Math.random()),
Links: []
})
const childCid = await ipfs.block.put(child, {
format: 'dag-pb',
version: 0
})
const parent = dagPB.encode({
Links: [{
Hash: childCid,
Tsize: child.length,
Name: ''
}]
})
const parentCid = await ipfs.block.put(parent, {
format: 'dag-pb',
version: 0
})
const grandParent = dagCBOR.encode({
parent: parentCid
})
const grandParentCid = await await ipfs.block.put(grandParent, {
format: 'dag-cbor',
version: 1
})
const expectedCids = [
grandParentCid,
parentCid,
childCid
]
const reader = await CarReader.fromIterable(ipfs.dag.export(grandParentCid))
const cids = await all(reader.cids())
expect(cids).to.deep.equal(expectedCids)
})
it('export of shuffled devnet export identical to canonical original', async function () {
this.timeout(360000)
const input = loadFixture('test/fixtures/car/lotus_devnet_genesis.car', 'interface-ipfs-core')
const result = await all(ipfs.dag.import(async function * () { yield input }()))
const exported = await toBuffer(ipfs.dag.export(result[0].root.cid))
expect(exported).to.equalBytes(input)
})
it('export of shuffled testnet export identical to canonical original', async function () {
this.timeout(360000)
const input = loadFixture('test/fixtures/car/lotus_testnet_export_128.car', 'interface-ipfs-core')
const result = await all(ipfs.dag.import(async function * () { yield input }()))
const exported = await toBuffer(ipfs.dag.export(result[0].root.cid))
expect(exported).to.equalBytes(input)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/dag/get.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import * as dagPB from '@ipld/dag-pb'
import * as dagCBOR from '@ipld/dag-cbor'
import * as dagJOSE from 'dag-jose'
import { importer } from 'ipfs-unixfs-importer'
import { UnixFS } from 'ipfs-unixfs'
import all from 'it-all'
import { CID } from 'multiformats/cid'
import { sha256 } from 'multiformats/hashes/sha2'
import { base32 } from 'multiformats/bases/base32'
import { base64url } from 'multiformats/bases/base64'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import testTimeout from '../utils/test-timeout.js'
import { identity } from 'multiformats/hashes/identity'
import blockstore from '../utils/blockstore-adapter.js'
import { ES256KSigner, createJWS } from 'did-jwt'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testGet (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.dag.get', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
/**
* @type {dagPB.PBNode}
*/
let pbNode
/**
* @type {any}
*/
let cborNode
/**
* @type {dagJOSE.DagJWE}
*/
let joseNode
/**
* @type {dagPB.PBNode}
*/
let nodePb
/**
* @type {any}
*/
let nodeCbor
/**
* @type {string}
*/
let nodeJose
/**
* @type {CID}
*/
let cidPb
/**
* @type {CID}
*/
let cidCbor
/**
* @type {CID}
*/
let cidJose
before(async () => {
const someData = uint8ArrayFromString('some other data')
pbNode = {
Data: someData,
Links: []
}
cborNode = {
data: someData
}
joseNode = {
protected: 'eyJhbGciOiJkaXIiLCJlbmMiOiJYQzIwUCJ9',
iv: 'DhVb9URR_o_85MOl-hCellwPTtQ_dj6d',
ciphertext: 'EtUsNJcKzEKdFM9DW5Ua5tVyaQRCKsAD',
tag: '-vG17pRSVB2Vycf2MZRgBA'
}
nodePb = {
Data: uint8ArrayFromString('I am inside a Protobuf'),
Links: []
}
cidPb = CID.createV1(dagPB.code, await sha256.digest(dagPB.encode(nodePb)))
nodeCbor = {
someData: 'I am inside a Cbor object',
pb: cidPb
}
cidCbor = CID.createV1(dagCBOR.code, await sha256.digest(dagCBOR.encode(nodeCbor)))
await ipfs.dag.put(nodePb, { storeCodec: 'dag-pb', hashAlg: 'sha2-256' })
await ipfs.dag.put(nodeCbor, { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' })
const signer = ES256KSigner(uint8ArrayFromString('278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f', 'hex'))
nodeJose = await createJWS(base64url.encode(cidCbor.bytes).slice(1), signer)
cidJose = CID.createV1(dagJOSE.code, await sha256.digest(dagJOSE.encode(nodeJose)))
await ipfs.dag.put(nodeJose, { storeCodec: dagJOSE.name, hashAlg: 'sha2-256' })
})
it('should respect timeout option when getting a DAG node', () => {
return testTimeout(() => ipfs.dag.get(CID.parse('QmPv52ekjS75L4JmHpXVeuJ5uX2ecSfSZo88NSyxwA3rAd'), {
timeout: 1
}))
})
it('should get a dag-pb node', async () => {
const cid = await ipfs.dag.put(pbNode, {
storeCodec: 'dag-pb',
hashAlg: 'sha2-256'
})
const result = await ipfs.dag.get(cid)
const node = result.value
expect(pbNode).to.eql(node)
})
it('should get a dag-cbor node', async () => {
const cid = await ipfs.dag.put(cborNode, {
storeCodec: 'dag-cbor',
hashAlg: 'sha2-256'
})
const result = await ipfs.dag.get(cid)
const node = result.value
expect(cborNode).to.eql(node)
})
it('should get a dag-pb node with path', async () => {
const result = await ipfs.dag.get(cidPb, {
path: '/'
})
const node = result.value
const cid = CID.createV1(dagPB.code, await sha256.digest(dagPB.encode(node)))
expect(cid.equals(cidPb)).to.be.true()
})
it('should get a dag-pb node local value', async function () {
const result = await ipfs.dag.get(cidPb, {
path: 'Data'
})
expect(result.value).to.eql(uint8ArrayFromString('I am inside a Protobuf'))
})
it.skip('should get a dag-pb node value one level deep', (done) => {})
it.skip('should get a dag-pb node value two levels deep', (done) => {})
it('should get a dag-cbor node with path', async () => {
const result = await ipfs.dag.get(cidCbor, {
path: '/'
})
const node = result.value
const cid = CID.createV1(dagCBOR.code, await sha256.digest(dagCBOR.encode(node)))
expect(cid.equals(cidCbor)).to.be.true()
})
it('should get a dag-cbor node local value', async () => {
const result = await ipfs.dag.get(cidCbor, {
path: 'someData'
})
expect(result.value).to.eql('I am inside a Cbor object')
})
it.skip('should get dag-cbor node value one level deep', (done) => {})
it.skip('should get dag-cbor node value two levels deep', (done) => {})
it.skip('should get dag-cbor value via dag-pb node', (done) => {})
it('should get only a CID, due to resolving locally only', async function () {
const result = await ipfs.dag.get(cidCbor, {
path: 'pb/Data',
localResolve: true
})
expect(result.value.equals(cidPb)).to.be.true()
})
it('should get dag-pb value via dag-cbor node', async function () {
const result = await ipfs.dag.get(cidCbor, {
path: 'pb/Data'
})
expect(result.value).to.eql(uint8ArrayFromString('I am inside a Protobuf'))
})
it('should get by CID with path option', async function () {
const result = await ipfs.dag.get(cidCbor, { path: '/pb/Data' })
expect(result.value).to.eql(uint8ArrayFromString('I am inside a Protobuf'))
})
it('should get only a CID, due to resolving locally only', async function () {
const result = await ipfs.dag.get(cidCbor, {
path: 'pb/Data',
localResolve: true
})
expect(result.value.equals(cidPb)).to.be.true()
})
it('should get with options and no path', async function () {
const result = await ipfs.dag.get(cidCbor, { localResolve: true })
expect(result.value).to.deep.equal(nodeCbor)
})
it('should get a node added as CIDv0 with a CIDv1', async () => {
const input = uint8ArrayFromString(`TEST${Math.random()}`)
const node = {
Data: input,
Links: []
}
const cid = await ipfs.dag.put(node, {
storeCodec: 'dag-pb',
hashAlg: 'sha2-256',
version: 0
})
expect(cid.version).to.equal(0)
const cidv1 = cid.toV1()
const output = await ipfs.dag.get(cidv1)
expect(output.value.Data).to.eql(input)
})
it('should get a node added as CIDv1 with a CIDv0', async () => {
const input = uint8ArrayFromString(`TEST${Math.random()}`)
const res = await all(importer([{ content: input }], blockstore(ipfs), {
cidVersion: 1,
rawLeaves: false
}))
const cidv1 = res[0].cid
expect(cidv1.version).to.equal(1)
const cidv0 = cidv1.toV0()
const output = await ipfs.dag.get(cidv0)
expect(UnixFS.unmarshal(output.value.Data).data).to.eql(input)
})
it('should be able to get part of a dag-cbor node', async () => {
const cbor = {
foo: 'dag-cbor-bar'
}
const cid = await ipfs.dag.put(cbor, { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' })
expect(cid.code).to.equal(dagCBOR.code)
expect(cid.toString(base32)).to.equal('bafyreic6f672hnponukaacmk2mmt7vs324zkagvu4hcww6yba6kby25zce')
const result = await ipfs.dag.get(cid, {
path: 'foo'
})
expect(result.value).to.equal('dag-cbor-bar')
})
it('should be able to traverse from one dag-cbor node to another', async () => {
const cbor1 = {
foo: 'dag-cbor-bar'
}
const cid1 = await ipfs.dag.put(cbor1, { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' })
const cbor2 = { other: cid1 }
const cid2 = await ipfs.dag.put(cbor2, { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' })
const result = await ipfs.dag.get(cid2, {
path: 'other/foo'
})
expect(result.value).to.equal('dag-cbor-bar')
})
it('should be able to get a DAG node with format raw', async () => {
const buf = Uint8Array.from([0, 1, 2, 3])
const cid = await ipfs.dag.put(buf, {
storeCodec: 'raw',
hashAlg: 'sha2-256'
})
const result = await ipfs.dag.get(cid)
expect(result.value).to.deep.equal(buf)
})
it('should be able to get a dag-cbor node with the identity hash', async () => {
const identityData = uint8ArrayFromString('A16461736466190144', 'base16upper')
const identityHash = await identity.digest(identityData)
const identityCID = CID.createV1(identity.code, identityHash)
const result = await ipfs.dag.get(identityCID)
expect(result.value).to.deep.equal(identityData)
})
it('should throw error for invalid string CID input', () => {
// @ts-expect-error invalid arg
return expect(ipfs.dag.get('INVALID CID'))
.to.eventually.be.rejected()
})
it('should throw error for invalid buffer CID input', () => {
// @ts-expect-error invalid arg
return expect(ipfs.dag.get(uint8ArrayFromString('INVALID CID')))
.to.eventually.be.rejected()
})
it('should return nested content when getting a CID with a path', async () => {
const regularContent = { test: '123' }
const cid1 = await ipfs.dag.put(regularContent)
const linkedContent = { link: cid1 }
const cid2 = await ipfs.dag.put(linkedContent)
const atPath = await ipfs.dag.get(cid2, { path: '/link' })
expect(atPath).to.have.deep.property('value', regularContent)
})
it('should not return nested content when getting a CID with a path and localResolve is true', async () => {
const regularContent = { test: '123' }
const cid1 = await ipfs.dag.put(regularContent)
const linkedContent = { link: cid1 }
const cid2 = await ipfs.dag.put(linkedContent)
const atPath = await ipfs.dag.get(cid2, { path: '/link', localResolve: true })
expect(atPath).to.have.deep.property('value').that.is.an.instanceOf(CID)
})
it('should get a dag-jose node', async () => {
const cid = await ipfs.dag.put(joseNode, {
storeCodec: 'dag-jose',
hashAlg: 'sha2-256'
})
const result = await ipfs.dag.get(cid)
const node = result.value
expect(joseNode).to.eql(node)
})
it('should get a dag-jose node with path', async () => {
const result = await ipfs.dag.get(cidJose, {
path: '/'
})
const node = result.value
const cid = CID.createV1(dagJOSE.code, await sha256.digest(dagJOSE.encode(node)))
expect(cid.equals(cidJose)).to.be.true()
})
it('should get a dag-jose node local value', async () => {
const result = await ipfs.dag.get(cidJose, {
path: 'payload'
})
const converted = dagJOSE.toGeneral(nodeJose)
expect(result.value).to.eql('payload' in converted && converted.payload)
})
it('should get dag-cbor value via dag-jose node', async function () {
const result = await ipfs.dag.get(cidJose, {
path: 'link/someData'
})
expect(result.value).to.eql('I am inside a Cbor object')
})
it('should get dag-cbor cid via dag-jose node if local resolve', async function () {
const result = await ipfs.dag.get(cidJose, {
path: 'link',
localResolve: true
})
expect(result.value).to.eql(cidCbor)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/dag/import.js
================================================
/* eslint-env mocha */
import all from 'it-all'
import drain from 'it-drain'
import { CID } from 'multiformats/cid'
import { sha256 } from 'multiformats/hashes/sha2'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { CarWriter, CarReader } from '@ipld/car'
import * as raw from 'multiformats/codecs/raw'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import loadFixture from 'aegir/fixtures'
/**
*
* @param {number} num
*/
async function createBlocks (num) {
const blocks = []
for (let i = 0; i < num; i++) {
const bytes = uint8ArrayFromString('block-' + Math.random())
const digest = await sha256.digest(raw.encode(bytes))
const cid = CID.create(1, raw.code, digest)
blocks.push({ bytes, cid })
}
return blocks
}
/**
* @param {{ cid: CID, bytes: Uint8Array }[]} blocks
* @returns {Promise>}
*/
async function createCar (blocks) {
const rootBlock = blocks[0]
const { writer, out } = await CarWriter.create([rootBlock.cid])
writer.put(rootBlock)
.then(async () => {
for (const block of blocks.slice(1)) {
writer.put(block)
}
await writer.close()
})
return out
}
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testImport (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.dag.import', function () {
this.timeout(540 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should import a car file', async () => {
const blocks = await createBlocks(5)
const car = await createCar(blocks)
const result = await all(ipfs.dag.import(car))
expect(result).to.have.lengthOf(1)
// @ts-expect-error chai types are messed up
expect(result).to.have.nested.deep.property('[0].root.cid', blocks[0].cid)
for (const { cid } of blocks) {
await expect(ipfs.block.get(cid)).to.eventually.be.ok()
}
await expect(all(ipfs.pin.ls({ paths: blocks[0].cid }))).to.eventually.have.lengthOf(1)
.and.have.nested.property('[0].type', 'recursive')
})
it('should import a car file without pinning the roots', async () => {
const blocks = await createBlocks(5)
const car = await createCar(blocks)
await all(ipfs.dag.import(car, {
pinRoots: false
}))
await expect(all(ipfs.pin.ls({ paths: blocks[0].cid }))).to.eventually.be.rejectedWith(/is not pinned/)
})
it('should import multiple car files', async () => {
const blocks1 = await createBlocks(5)
const car1 = await createCar(blocks1)
const blocks2 = await createBlocks(5)
const car2 = await createCar(blocks2)
const result = await all(ipfs.dag.import([car1, car2]))
expect(result).to.have.lengthOf(2)
expect(result).to.deep.include({ root: { cid: blocks1[0].cid, pinErrorMsg: '' } })
expect(result).to.deep.include({ root: { cid: blocks2[0].cid, pinErrorMsg: '' } })
for (const { cid } of blocks1) {
await expect(ipfs.block.get(cid)).to.eventually.be.ok()
}
for (const { cid } of blocks2) {
await expect(ipfs.block.get(cid)).to.eventually.be.ok()
}
})
it('should import car with roots but no blocks', async () => {
const input = loadFixture('test/fixtures/car/combined_naked_roots_genesis_and_128.car', 'interface-ipfs-core')
const reader = await CarReader.fromBytes(input)
const cids = await reader.getRoots()
expect(cids).to.have.lengthOf(2)
// naked roots car does not contain blocks
const result1 = await all(ipfs.dag.import(async function * () { yield input }()))
expect(result1).to.deep.include({ root: { cid: cids[0], pinErrorMsg: 'blockstore: block not found' } })
expect(result1).to.deep.include({ root: { cid: cids[1], pinErrorMsg: 'blockstore: block not found' } })
await drain(ipfs.dag.import(async function * () { yield loadFixture('test/fixtures/car/lotus_devnet_genesis_shuffled_nulroot.car', 'interface-ipfs-core') }()))
// have some of the blocks now, should be able to pin one root
const result2 = await all(ipfs.dag.import(async function * () { yield input }()))
expect(result2).to.deep.include({ root: { cid: cids[0], pinErrorMsg: '' } })
expect(result2).to.deep.include({ root: { cid: cids[1], pinErrorMsg: 'blockstore: block not found' } })
await drain(ipfs.dag.import(async function * () { yield loadFixture('test/fixtures/car/lotus_testnet_export_128.car', 'interface-ipfs-core') }()))
// have all of the blocks now, should be able to pin both
const result3 = await all(ipfs.dag.import(async function * () { yield input }()))
expect(result3).to.deep.include({ root: { cid: cids[0], pinErrorMsg: '' } })
expect(result3).to.deep.include({ root: { cid: cids[1], pinErrorMsg: '' } })
})
it('should import lotus devnet genesis shuffled nulroot', async () => {
const input = loadFixture('test/fixtures/car/lotus_devnet_genesis_shuffled_nulroot.car', 'interface-ipfs-core')
const reader = await CarReader.fromBytes(input)
const cids = await reader.getRoots()
expect(cids).to.have.lengthOf(1)
expect(cids[0].toString()).to.equal('bafkqaaa')
const result = await all(ipfs.dag.import(async function * () { yield input }()))
// @ts-expect-error chai types are messed up
expect(result).to.have.nested.deep.property('[0].root.cid', cids[0])
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/dag/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testExport } from './export.js'
import { testGet } from './get.js'
import { testPut } from './put.js'
import { testImport } from './import.js'
import { testResolve } from './resolve.js'
import { testDagSharnessT0053 } from './sharness-t0053-dag.js'
const tests = {
export: testExport,
get: testGet,
put: testPut,
import: testImport,
resolve: testResolve,
dagSharnessT0053: testDagSharnessT0053
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/dag/put.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import * as dagCBOR from '@ipld/dag-cbor'
import { CID } from 'multiformats/cid'
import { sha256, sha512 } from 'multiformats/hashes/sha2'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testPut (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.dag.put', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
const pbNode = {
Data: uint8ArrayFromString('some data'),
Links: []
}
const cborNode = {
data: uint8ArrayFromString('some other data')
}
const joseNode = 'eyJhbGciOiJFUzI1NksifQ.AXESICjDGMg3fEBSX7_fpbBUYF4E61TXLysmLJgfGEpFG8Pu.z7a2MvPWLsd7leOeHyfeA1OcAFC9yy5rn1HD8xCeHz3nFrwyn_Su5xXUoaIxAre3fXhGjPkVSNiCE36AKiaMng'
it('should put dag-pb with default hash func (sha2-256)', () => {
return ipfs.dag.put(pbNode, {
storeCodec: 'dag-pb',
hashAlg: 'sha2-256'
})
})
it('should put dag-pb with non-default hash func (sha2-512)', () => {
return ipfs.dag.put(pbNode, {
storeCodec: 'dag-pb',
hashAlg: 'sha2-512'
})
})
it('should put dag-cbor with default hash func (sha2-256)', () => {
return ipfs.dag.put(cborNode, {
storeCodec: 'dag-cbor',
hashAlg: 'sha2-256'
})
})
it('should put dag-cbor with non-default hash func (sha2-512)', () => {
return ipfs.dag.put(cborNode, {
storeCodec: 'dag-cbor',
hashAlg: 'sha2-512'
})
})
it('should put dag-jose with default hash func (sha2-256)', () => {
return ipfs.dag.put(joseNode, {
storeCodec: 'dag-jose',
hashAlg: 'sha2-256'
})
})
it('should put dag-jose with non-default hash func (sha2-512)', () => {
return ipfs.dag.put(joseNode, {
storeCodec: 'dag-jose',
hashAlg: 'sha2-512'
})
})
it('should return the cid', async () => {
const cid = await ipfs.dag.put(cborNode, {
storeCodec: 'dag-cbor',
hashAlg: 'sha2-256'
})
expect(cid).to.exist()
expect(cid).to.be.an.instanceOf(CID)
const bytes = dagCBOR.encode(cborNode)
const hash = await sha256.digest(bytes)
const _cid = CID.createV1(dagCBOR.code, hash)
expect(cid.bytes).to.eql(_cid.bytes)
})
it('should not fail when calling put without options', () => {
return ipfs.dag.put(cborNode)
})
it('should set defaults when calling put without options', async () => {
const cid = await ipfs.dag.put(cborNode)
expect(cid.code).to.equal(dagCBOR.code)
expect(cid.multihash.code).to.equal(sha256.code)
})
it('should override hash algorithm default and resolve with it', async () => {
const cid = await ipfs.dag.put(cborNode, {
storeCodec: 'dag-cbor',
hashAlg: 'sha2-512'
})
expect(cid.code).to.equal(dagCBOR.code)
expect(cid.multihash.code).to.equal(sha512.code)
})
it.skip('should put by passing the cid instead of format and hashAlg', (done) => {})
})
}
================================================
FILE: packages/interface-ipfs-core/src/dag/resolve.js
================================================
/* eslint-env mocha */
import * as dagPB from '@ipld/dag-pb'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import testTimeout from '../utils/test-timeout.js'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testResolve (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.dag.resolve', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('should respect timeout option when resolving a path within a DAG node', async () => {
const cid = await ipfs.dag.put({}, { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' })
return testTimeout(() => ipfs.dag.resolve(cid, {
timeout: 1
}))
})
it('should resolve a path inside a cbor node', async () => {
const obj = {
a: 1,
b: [1, 2, 3],
c: {
ca: [5, 6, 7],
cb: 'foo'
}
}
const cid = await ipfs.dag.put(obj, { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' })
const result = await ipfs.dag.resolve(`${cid}/c/cb`)
expect(result).to.have.deep.property('cid', cid)
expect(result).to.have.property('remainderPath', 'c/cb')
})
it('should resolve a path inside a cbor node by CID', async () => {
const obj = {
a: 1,
b: [1, 2, 3],
c: {
ca: [5, 6, 7],
cb: 'foo'
}
}
const cid = await ipfs.dag.put(obj, { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' })
const result = await ipfs.dag.resolve(cid, { path: '/c/cb' })
expect(result).to.have.deep.property('cid', cid)
expect(result).to.have.property('remainderPath', 'c/cb')
})
it('should resolve a multi-node path inside a cbor node', async () => {
const obj0 = {
ca: [5, 6, 7],
cb: 'foo'
}
const cid0 = await ipfs.dag.put(obj0, { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' })
const obj1 = {
a: 1,
b: [1, 2, 3],
c: cid0
}
const cid1 = await ipfs.dag.put(obj1, { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' })
const result = await ipfs.dag.resolve(`/ipfs/${cid1}/c/cb`)
expect(result).to.have.deep.property('cid', cid0)
expect(result).to.have.property('remainderPath', 'cb')
})
it('should resolve a multi-node path inside a cbor node by CID', async () => {
const obj0 = {
ca: [5, 6, 7],
cb: 'foo'
}
const cid0 = await ipfs.dag.put(obj0, { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' })
const obj1 = {
a: 1,
b: [1, 2, 3],
c: cid0
}
const cid1 = await ipfs.dag.put(obj1, { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' })
const result = await ipfs.dag.resolve(cid1, { path: '/c/cb' })
expect(result).to.have.deep.property('cid', cid0)
expect(result).to.have.property('remainderPath', 'cb')
})
it('should resolve a raw node', async () => {
const node = uint8ArrayFromString('hello world')
const cid = await ipfs.dag.put(node, { storeCodec: 'raw', hashAlg: 'sha2-256' })
const result = await ipfs.dag.resolve(cid, { path: '/' })
expect(result).to.have.deep.property('cid', cid)
expect(result).to.have.property('remainderPath', '')
})
it('should resolve a path inside a dag-pb node linked to from another dag-pb node', async () => {
const someData = uint8ArrayFromString('some other data')
const childNode = {
Data: someData,
Links: []
}
const childCid = await ipfs.dag.put(childNode, { storeCodec: 'dag-pb', hashAlg: 'sha2-256' })
const linkToChildNode = {
Name: 'foo',
Tsize: dagPB.encode(childNode).length,
Hash: childCid
}
const parentNode = {
Data: uint8ArrayFromString('derp'),
Links: [linkToChildNode]
}
const parentCid = await ipfs.dag.put(parentNode, { storeCodec: 'dag-pb', hashAlg: 'sha2-256' })
const result = await ipfs.dag.resolve(parentCid, { path: '/foo' })
expect(result).to.have.deep.property('cid', childCid)
expect(result).to.have.property('remainderPath', '')
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/dag/sharness-t0053-dag.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { base64pad } from 'multiformats/bases/base64'
import { base58btc } from 'multiformats/bases/base58'
import { CID } from 'multiformats'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testDagSharnessT0053 (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.dag (sharness-t0053-dag)', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
/** @type {CID} */
let hash1
/** @type {CID} */
let hash2
/** @type {CID} */
let hash3
/** @type {CID} */
let hash4
/** @type {Uint8Array} */
let ipldObject
/** @type {Uint8Array} */
let ipldObjectDagCbor
/** @type {Uint8Array} */
let ipldObjectDagPb
/** @type {Uint8Array} */
let ipldObjectDagJson
const ipldHash = 'bafyreiblwimnjbqcdoeafiobk6q27jcw64ew7n2fmmhdpldd63edmjecde'
const ipldDagCborHash = 'bafyreieculsmrexh3ty5jentbvuku452o27mst4h2tq2rb2zntqhgcstji'
const ipldDagJsonHash = 'baguqeerajwksxu3lxpomdwxvosl542zl3xknhjgxtq3277gafrhl6vdw5tcq'
const ipldDagPbHash = 'bafybeibazl2z4vqp2tmwcfag6wirmtpnomxknqcgrauj7m2yisrz3qjbom'
before(async () => {
hash1 = (await ipfs.add({ content: 'foo\n', path: 'file1' })).cid
hash2 = (await ipfs.add({ content: 'bar\n', path: 'file2' })).cid
hash3 = (await ipfs.add({ content: 'baz\n', path: 'file3' })).cid
hash4 = (await ipfs.add({ content: 'qux\n', path: 'file4' })).cid
ipldObject = new TextEncoder().encode(`{"hello":"world","cats":[{"/":"${hash1}"},{"water":{"/":"${hash2}"}}],"magic":{"/":"${hash3}"},"sub":{"dict":"ionary","beep":[0,"bop"]}}`)
ipldObjectDagCbor = base64pad.decode('MomREYXRhRQABAgMEZUxpbmtzgA==')
ipldObjectDagPb = base64pad.decode('MCgUAAQIDBA==')
ipldObjectDagJson = new TextEncoder().encode('{"Data":{"/":{"bytes":"AAECAwQ"}},"Links":[]}')
})
it('sanity check', () => {
expect(hash1.toString()).to.equal('QmYNmQKp6SuaVrpgWRsPTgCQCnpxUYGq76YEKBXuj2N4H6')
expect(hash2.toString()).to.equal('QmTz3oc4gdpRMKP2sdGUPZTAGRngqjsi99BPoztyP53JMM')
expect(hash3.toString()).to.equal('QmWLdkp93sNxGRjnFHPaYg8tCQ35NBY3XPn6KiETd3Z4WR')
expect(hash4.toString()).to.equal('QmZCoKN8vvRbxfn4BMG9678UQTSUwPXRJsRA9jnjoucHUj')
})
it('can add an ipld object using defaults (dag-json to dag-cbor)', async () => {
// dag-json is default on CLI, force it to interpret our bytes here
const cid = await ipfs.dag.put(ipldObject, { inputCodec: 'dag-json' })
expect(cid.toString()).to.equal(ipldHash)
})
it('can add an ipld object using dag-json to dag-json', async () => {
const cid = await ipfs.dag.put(ipldObject, { inputCodec: 'dag-json', storeCodec: 'dag-json' })
expect(cid.toString()).to.equal('baguqeera6gviseelmbzn2ugoddo5vulxlshqs3kw5ymgsb6w4cabnoh4ldpa')
})
it('can add an ipld object using dag-json to dag-cbor', async () => {
const cid = await ipfs.dag.put(ipldObject, { inputCodec: 'dag-json', storeCodec: 'dag-cbor' })
expect(cid.toString()).to.equal(ipldHash)
})
// this is not testing what the upstream sharness is testing since we're converting it locally
// and not asking the CLI for it, but it's included for completeness
it('can add an ipld object using cid-base=base58btc', async () => {
const cid = await ipfs.dag.put(ipldObject, { inputCodec: 'dag-json' })
expect(cid.toString(base58btc)).to.equal('zdpuAoN1XJ3GsrxEzMuCbRKZzRUVJekJUCbPVgCgE4D9yYqVi')
})
// (1) dag-cbor input
it('can add a dag-cbor input block stored as dag-cbor', async () => {
const cid = await ipfs.dag.put(ipldObjectDagCbor, { inputCodec: 'dag-cbor', storeCodec: 'dag-cbor' })
expect(cid.toString()).to.equal(ipldDagCborHash)
})
it('can add a dag-cbor input block stored as dag-pb', async () => {
const cid = await ipfs.dag.put(ipldObjectDagCbor, { inputCodec: 'dag-cbor', storeCodec: 'dag-pb' })
expect(cid.toString()).to.equal(ipldDagPbHash)
})
it('can add a dag-cbor input block stored as dag-json', async () => {
const cid = await ipfs.dag.put(ipldObjectDagCbor, { inputCodec: 'dag-cbor', storeCodec: 'dag-json' })
expect(cid.toString()).to.equal(ipldDagJsonHash)
})
// (2) dag-json input
it('can add a dag-json input block stored as dag-cbor', async () => {
const cid = await ipfs.dag.put(ipldObjectDagJson, { inputCodec: 'dag-json', storeCodec: 'dag-cbor' })
expect(cid.toString()).to.equal(ipldDagCborHash)
})
it('can add a dag-json input block stored as dag-pb', async () => {
const cid = await ipfs.dag.put(ipldObjectDagJson, { inputCodec: 'dag-json', storeCodec: 'dag-pb' })
expect(cid.toString()).to.equal(ipldDagPbHash)
})
it('can add a dag-json input block stored as dag-json', async () => {
const cid = await ipfs.dag.put(ipldObjectDagJson, { inputCodec: 'dag-json', storeCodec: 'dag-json' })
expect(cid.toString()).to.equal(ipldDagJsonHash)
})
// (3) dag-pb input
it('can add a dag-pb input block stored as dag-cbor', async () => {
const cid = await ipfs.dag.put(ipldObjectDagPb, { inputCodec: 'dag-pb', storeCodec: 'dag-cbor' })
expect(cid.toString()).to.equal(ipldDagCborHash)
})
it('can add a dag-pb input block stored as dag-pb', async () => {
const cid = await ipfs.dag.put(ipldObjectDagPb, { inputCodec: 'dag-pb', storeCodec: 'dag-pb' })
expect(cid.toString()).to.equal(ipldDagPbHash)
})
it('can add a dag-pb input block stored as dag-json', async () => {
const cid = await ipfs.dag.put(ipldObjectDagPb, { inputCodec: 'dag-pb', storeCodec: 'dag-json' })
expect(cid.toString()).to.equal(ipldDagJsonHash)
})
it('can get dag-cbor, dag-json, dag-pb blocks as dag-json', async () => {
const resultCbor = await ipfs.dag.get(CID.parse(ipldDagCborHash))
const resultJson = await ipfs.dag.get(CID.parse(ipldDagJsonHash))
const resultPb = await ipfs.dag.get(CID.parse(ipldDagPbHash))
expect(resultCbor).to.deep.equal(resultJson)
expect(resultCbor).to.deep.equal(resultPb)
})
/*
This is illustrative only - it's not testing anything meaningful. It's supposed to test
`outputCodec` which isn't supported for the http client or core since we get the decoded JS
form of the node. But this test code as it's written is doing the encode locally and
asserting on that .. which is just testing the codec.
it('can get dag-pb block transcoded as dag-cbor', async () => {
const { value } = await ipfs.dag.get(CID.parse(ipldDagPbHash), { outputCodec: 'dag-cbor' })
const block = await Block.encode({ value, codec: dagCbor, hasher: sha256 })
expect(bytes.toHex(block.cid.multihash.bytes)).to.equal('122082a2e4c892e7dcf1d491b30d68aa73ba76bec94f87d4e1a887596ce0730a534a')
})
*/
// Skipped: 'dag put and dag get transcodings match' - tests the round-trip of the above
it('resolving sub-objects works', async () => {
let result = await ipfs.dag.get(CID.parse(ipldHash), { path: 'hello' })
expect(result.value).to.equal('world')
result = await ipfs.dag.get(CID.parse(ipldHash), { path: 'sub' })
expect(result.value).to.deep.equal({ beep: [0, 'bop'], dict: 'ionary' })
result = await ipfs.dag.get(CID.parse(ipldHash), { path: 'sub/beep' })
expect(result.value).to.deep.equal([0, 'bop'])
result = await ipfs.dag.get(CID.parse(ipldHash), { path: 'sub/beep/0' })
expect(result.value).to.equal(0)
result = await ipfs.dag.get(CID.parse(ipldHash), { path: 'sub/beep/1' })
expect(result.value).to.equal('bop')
})
// Skipped: 'traversals using /ipld/ work' - not implemented here, yet?
// Skipped additional pin, resolve and other tests
})
}
================================================
FILE: packages/interface-ipfs-core/src/dht/disabled.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import all from 'it-all'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testDisabled (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('disabled', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let nodeA
/** @type {import('ipfs-core-types').IPFS} */
let nodeB
before(async () => {
nodeA = (await factory.spawn({
ipfsOptions: {
config: {
Routing: {
Type: 'none'
}
}
}
})).api
nodeB = (await factory.spawn()).api
const nodeBId = await nodeB.id()
await nodeA.swarm.connect(nodeBId.addresses[0])
})
after(() => factory.clean())
it('should error when DHT not available', async () => {
await expect(all(nodeA.dht.put('/ipns/12D3KooWBD9zgsogrYf1dum1TwTwe6k5xT8acGZ5PNeYmKf72qz2', uint8ArrayFromString('hello'), { verbose: true })))
.to.eventually.be.rejected()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/dht/find-peer.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import testTimeout from '../utils/test-timeout.js'
import drain from 'it-drain'
import all from 'it-all'
import { ensureReachable } from './utils.js'
import { peerIdFromString } from '@libp2p/peer-id'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testFindPeer (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.dht.findPeer', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let nodeA
/** @type {import('ipfs-core-types').IPFS} */
let nodeB
before(async () => {
nodeA = (await factory.spawn()).api
nodeB = (await factory.spawn()).api
await ensureReachable(nodeA, nodeB)
})
after(() => factory.clean())
it('should respect timeout option when finding a peer on the DHT', async () => {
const nodeBId = await nodeB.id()
await testTimeout(() => drain(nodeA.dht.findPeer(nodeBId.id, {
timeout: 1
})))
})
it('should find other peers', async () => {
const nodeBId = await nodeB.id()
const results = await all(nodeA.dht.findPeer(nodeBId.id))
const finalPeer = results.filter(event => event.name === 'FINAL_PEER').pop()
if (!finalPeer || finalPeer.name !== 'FINAL_PEER') {
throw new Error('No finalPeer event received')
}
const id = finalPeer.peer.id
const nodeAddresses = nodeBId.addresses.map((addr) => addr.nodeAddress())
const peerAddresses = finalPeer.peer.multiaddrs.map(ma => ma.nodeAddress())
expect(id.toString()).to.equal(nodeBId.id.toString())
expect(peerAddresses).to.deep.include(nodeAddresses[0])
})
it('should fail to find other peer if peer does not exist', async () => {
const events = await all(nodeA.dht.findPeer(peerIdFromString('Qmd7qZS4T7xXtsNFdRoK1trfMs5zU94EpokQ9WFtxdPxsZ')))
// no finalPeer events found
expect(events.filter(event => event.name === 'FINAL_PEER')).to.be.empty()
// queryError events found
expect(events.filter(event => event.name === 'QUERY_ERROR')).to.not.be.empty()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/dht/find-provs.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import all from 'it-all'
import drain from 'it-drain'
import testTimeout from '../utils/test-timeout.js'
import { ensureReachable } from './utils.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testFindProvs (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.dht.findProvs', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let nodeA
/** @type {import('ipfs-core-types').IPFS} */
let nodeB
/** @type {import('ipfs-core-types').IPFS} */
let nodeC
before(async () => {
nodeA = (await factory.spawn()).api
nodeB = (await factory.spawn()).api
nodeC = (await factory.spawn()).api
await ensureReachable(nodeB, nodeA)
await ensureReachable(nodeC, nodeB)
})
after(() => factory.clean())
/**
* @type {import('multiformats/cid').CID}
*/
let providedCid
before('add providers for the same cid', async function () {
const cids = await Promise.all([
nodeB.object.new('unixfs-dir'),
nodeC.object.new('unixfs-dir')
])
providedCid = cids[0]
await Promise.all([
all(nodeB.dht.provide(providedCid)),
all(nodeC.dht.provide(providedCid))
])
})
it('should respect timeout option when finding providers on the DHT', () => {
return testTimeout(() => drain(nodeA.dht.findProvs(providedCid, {
timeout: 1
})))
})
it('should be able to find providers', async function () {
/** @type {string[]} */
const providerIds = []
for await (const event of nodeA.dht.findProvs(providedCid)) {
if (event.name === 'PROVIDER') {
providerIds.push(...event.providers.map(prov => prov.id.toString()))
}
}
const nodeBId = await nodeB.id()
const nodeCId = await nodeC.id()
expect(providerIds).to.include(nodeBId.id.toString())
expect(providerIds).to.include(nodeCId.id.toString())
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/dht/get.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import testTimeout from '../utils/test-timeout.js'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import drain from 'it-drain'
import all from 'it-all'
import { ensureReachable } from './utils.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testGet (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.dht.get', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let nodeA
/** @type {import('ipfs-core-types').IPFS} */
let nodeB
before(async () => {
nodeA = (await factory.spawn()).api
nodeB = (await factory.spawn()).api
await ensureReachable(nodeA, nodeB)
})
after(() => factory.clean())
it('should respect timeout option when getting a value from the DHT', async () => {
const data = await nodeA.add('should put a value to the DHT')
const publish = await nodeA.name.publish(data.cid)
await testTimeout(() => drain(nodeB.dht.get(`/ipns/${publish.name}`, {
timeout: 1
})))
})
it('should error when getting a non-existent key from the DHT', async () => {
const key = '/ipns/k51qzi5uqu5dl0dbfddy2wb42nvbc6anyxnkrguy5l0h0bv9kaih6j6vqdskqk'
const events = await all(nodeA.dht.get(key))
// no value events found
expect(events.filter(event => event.name === 'VALUE')).to.be.empty()
// queryError events found
expect(events.filter(event => event.name === 'QUERY_ERROR')).to.not.be.empty()
})
it('should get a value after it was put on another node', async () => {
const data = await nodeA.add('should put a value to the DHT')
const publish = await nodeA.name.publish(data.cid)
const events = await all(nodeA.dht.get(`/ipns/${publish.name}`))
const valueEvent = events.filter(event => event.name === 'VALUE').pop()
if (!valueEvent || valueEvent.name !== 'VALUE') {
throw new Error('Value event not found')
}
expect(uint8ArrayToString(valueEvent.value)).to.contain(data.cid.toString())
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/dht/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testPut } from './put.js'
import { testGet } from './get.js'
import { testFindPeer } from './find-peer.js'
import { testProvide } from './provide.js'
import { testFindProvs } from './find-provs.js'
import { testQuery } from './query.js'
import { testDisabled } from './disabled.js'
const tests = {
put: testPut,
get: testGet,
findPeer: testFindPeer,
provide: testProvide,
findProvs: testFindProvs,
query: testQuery,
disabled: testDisabled
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/dht/provide.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { CID } from 'multiformats/cid'
import all from 'it-all'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { ensureReachable } from './utils.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testProvide (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.dht.provide', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
const nodeB = (await factory.spawn()).api
await ensureReachable(ipfs, nodeB)
})
after(() => factory.clean())
it('should provide local CID', async () => {
const res = await ipfs.add(uint8ArrayFromString('test'))
await all(ipfs.dht.provide(res.cid))
})
it('should not provide if block not found locally', () => {
const cid = CID.parse('Qmd7qZS4T7xXtsNFdRoK1trfMs5zU94EpokQ9WFtxdPxsZ')
return expect(all(ipfs.dht.provide(cid))).to.eventually.be.rejected
.and.be.an.instanceOf(Error)
.and.have.property('message')
.that.include('not found locally')
})
it('should provide a CIDv1', async () => {
const res = await ipfs.add(uint8ArrayFromString('test'), { cidVersion: 1 })
await all(ipfs.dht.provide(res.cid))
})
it('should error on non CID arg', async () => {
// @ts-expect-error invalid arg
return expect(all(ipfs.dht.provide({}))).to.eventually.be.rejected()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/dht/put.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import all from 'it-all'
import { ensureReachable } from './utils.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testPut (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.dht.put', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let nodeA
/** @type {import('ipfs-core-types').IPFS} */
let nodeB
before(async () => {
nodeA = (await factory.spawn()).api
nodeB = (await factory.spawn()).api
await ensureReachable(nodeA, nodeB)
})
after(() => factory.clean())
it('should put a value to the DHT', async function () {
const { cid } = await nodeA.add('should put a value to the DHT')
const publish = await nodeA.name.publish(cid)
let record
for await (const event of nodeA.dht.get(`/ipns/${publish.name}`)) {
if (event.name === 'VALUE') {
record = event.value
break
}
}
if (!record) {
throw new Error('Could not find value')
}
const events = await all(nodeA.dht.put(`/ipns/${publish.name}`, record, { verbose: true }))
const peerResponse = events.filter(event => event.name === 'PEER_RESPONSE').pop()
if (!peerResponse || peerResponse.name !== 'PEER_RESPONSE') {
throw new Error('Did not get peer response')
}
const nodeBId = await nodeB.id()
expect(peerResponse.from.toString()).to.be.equal(nodeBId.id.toString())
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/dht/query.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import drain from 'it-drain'
import testTimeout from '../utils/test-timeout.js'
import { ensureReachable } from './utils.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testQuery (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.dht.query', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let nodeA
/** @type {import('ipfs-core-types').IPFS} */
let nodeB
before(async () => {
nodeA = (await factory.spawn()).api
nodeB = (await factory.spawn()).api
await ensureReachable(nodeA, nodeB)
})
after(() => factory.clean())
it('should respect timeout option when querying the DHT', async () => {
const nodeBId = await nodeB.id()
return testTimeout(() => drain(nodeA.dht.query(nodeBId.id, {
timeout: 1
})))
})
it('should return the other node in the query', async function () {
/** @type {string[]} */
const peers = []
const nodeBId = await nodeB.id()
for await (const event of nodeA.dht.query(nodeBId.id)) {
if (event.name === 'PEER_RESPONSE') {
peers.push(...event.closer.map(data => data.id.toString()))
}
}
expect(peers).to.include(nodeBId.id.toString())
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/dht/utils.js
================================================
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { CID } from 'multiformats/cid'
import { sha256 } from 'multiformats/hashes/sha2'
import delay from 'delay'
/**
* @param {Uint8Array} [data]
* @returns
*/
export async function fakeCid (data) {
const bytes = data || uint8ArrayFromString(`TEST${Math.random()}`)
const mh = await sha256.digest(bytes)
return CID.createV0(mh)
}
/**
* @param {import('ipfs-core-types').IPFS} nodeA
* @param {import('ipfs-core-types').IPFS} nodeB
*/
export async function ensureReachable (nodeA, nodeB) {
/**
* @param {import('ipfs-core-types').IPFS} source
* @param {import('ipfs-core-types').IPFS} target
*/
async function canFindOnDHT (source, target) {
const { id } = await target.id()
for await (const event of source.dht.query(id)) {
if (event.name === 'PEER_RESPONSE' && event.from.equals(id)) {
return
}
}
throw new Error(`Could not find ${id} in DHT`)
}
const nodeBId = await nodeB.id()
await nodeA.swarm.connect(nodeBId.addresses[0])
while (true) {
try {
await Promise.all([
canFindOnDHT(nodeA, nodeB),
canFindOnDHT(nodeB, nodeA)
])
break
} catch {
await delay(1000)
}
}
}
================================================
FILE: packages/interface-ipfs-core/src/files/chmod.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { nanoid } from 'nanoid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import isShardAtPath from '../utils/is-shard-at-path.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testChmod (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.files.chmod', function () {
this.timeout(120 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/**
* @param {string} initialMode
* @param {string} modification
* @param {string} expectedFinalMode
*/
async function testChmod (initialMode, modification, expectedFinalMode) {
const path = `/test-${nanoid()}`
await ipfs.files.write(path, uint8ArrayFromString('Hello world!'), {
create: true,
mtime: new Date(),
mode: initialMode
})
await ipfs.files.chmod(path, modification, {
flush: true
})
const updatedMode = (await ipfs.files.stat(path)).mode
expect(updatedMode).to.equal(parseInt(expectedFinalMode, 8))
}
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should update the mode for a file', async () => {
const path = `/foo-${Math.random()}`
await ipfs.files.write(path, uint8ArrayFromString('Hello world'), {
create: true,
mtime: new Date()
})
const originalMode = (await ipfs.files.stat(path)).mode
await ipfs.files.chmod(path, '0777', {
flush: true
})
const updatedMode = (await ipfs.files.stat(path)).mode
expect(updatedMode).to.not.equal(originalMode)
expect(updatedMode).to.equal(parseInt('0777', 8))
})
it('should update the mode for a directory', async () => {
const path = `/foo-${Math.random()}`
await ipfs.files.mkdir(path)
const originalMode = (await ipfs.files.stat(path)).mode
await ipfs.files.chmod(path, '0777', {
flush: true
})
const updatedMode = (await ipfs.files.stat(path)).mode
expect(updatedMode).to.not.equal(originalMode)
expect(updatedMode).to.equal(parseInt('0777', 8))
})
it('should update the mode for a hamt-sharded-directory', async () => {
const path = `/foo-${Math.random()}`
await ipfs.files.mkdir(path)
await ipfs.files.write(`${path}/foo.txt`, uint8ArrayFromString('Hello world'), {
create: true,
shardSplitThreshold: 0
})
const originalMode = (await ipfs.files.stat(path)).mode
await ipfs.files.chmod(path, '0777', {
flush: true
})
const updatedMode = (await ipfs.files.stat(path)).mode
expect(updatedMode).to.not.equal(originalMode)
expect(updatedMode).to.equal(parseInt('0777', 8))
})
it('should update modes with basic symbolic notation that adds bits', async () => {
await testChmod('0000', '+x', '0111')
await testChmod('0000', '+w', '0222')
await testChmod('0000', '+r', '0444')
await testChmod('0000', 'u+x', '0100')
await testChmod('0000', 'u+w', '0200')
await testChmod('0000', 'u+r', '0400')
await testChmod('0000', 'g+x', '0010')
await testChmod('0000', 'g+w', '0020')
await testChmod('0000', 'g+r', '0040')
await testChmod('0000', 'o+x', '0001')
await testChmod('0000', 'o+w', '0002')
await testChmod('0000', 'o+r', '0004')
await testChmod('0000', 'ug+x', '0110')
await testChmod('0000', 'ug+w', '0220')
await testChmod('0000', 'ug+r', '0440')
await testChmod('0000', 'ugo+x', '0111')
await testChmod('0000', 'ugo+w', '0222')
await testChmod('0000', 'ugo+r', '0444')
await testChmod('0000', 'a+x', '0111')
await testChmod('0000', 'a+w', '0222')
await testChmod('0000', 'a+r', '0444')
})
it('should update modes with basic symbolic notation that removes bits', async () => {
await testChmod('0111', '-x', '0000')
await testChmod('0222', '-w', '0000')
await testChmod('0444', '-r', '0000')
await testChmod('0100', 'u-x', '0000')
await testChmod('0200', 'u-w', '0000')
await testChmod('0400', 'u-r', '0000')
await testChmod('0010', 'g-x', '0000')
await testChmod('0020', 'g-w', '0000')
await testChmod('0040', 'g-r', '0000')
await testChmod('0001', 'o-x', '0000')
await testChmod('0002', 'o-w', '0000')
await testChmod('0004', 'o-r', '0000')
await testChmod('0110', 'ug-x', '0000')
await testChmod('0220', 'ug-w', '0000')
await testChmod('0440', 'ug-r', '0000')
await testChmod('0111', 'ugo-x', '0000')
await testChmod('0222', 'ugo-w', '0000')
await testChmod('0444', 'ugo-r', '0000')
await testChmod('0111', 'a-x', '0000')
await testChmod('0222', 'a-w', '0000')
await testChmod('0444', 'a-r', '0000')
})
it('should update modes with basic symbolic notation that overrides bits', async () => {
await testChmod('0777', '=x', '0111')
await testChmod('0777', '=w', '0222')
await testChmod('0777', '=r', '0444')
await testChmod('0777', 'u=x', '0177')
await testChmod('0777', 'u=w', '0277')
await testChmod('0777', 'u=r', '0477')
await testChmod('0777', 'g=x', '0717')
await testChmod('0777', 'g=w', '0727')
await testChmod('0777', 'g=r', '0747')
await testChmod('0777', 'o=x', '0771')
await testChmod('0777', 'o=w', '0772')
await testChmod('0777', 'o=r', '0774')
await testChmod('0777', 'ug=x', '0117')
await testChmod('0777', 'ug=w', '0227')
await testChmod('0777', 'ug=r', '0447')
await testChmod('0777', 'ugo=x', '0111')
await testChmod('0777', 'ugo=w', '0222')
await testChmod('0777', 'ugo=r', '0444')
await testChmod('0777', 'a=x', '0111')
await testChmod('0777', 'a=w', '0222')
await testChmod('0777', 'a=r', '0444')
})
it('should update modes with multiple symbolic notation', async () => {
await testChmod('0000', 'g+x,u+w', '0210')
})
it('should update modes with special symbolic notation', async () => {
await testChmod('0000', 'g+s', '2000')
await testChmod('0000', 'u+s', '4000')
await testChmod('0000', '+t', '1000')
await testChmod('0000', '+s', '6000')
})
it('should apply special execute permissions to world', async () => {
const path = `/foo-${Math.random()}`
const sub = `${path}/sub`
const file = `${path}/sub/foo.txt`
const bin = `${path}/sub/bar`
await ipfs.files.mkdir(sub, {
parents: true
})
await ipfs.files.touch(file)
await ipfs.files.touch(bin)
await ipfs.files.chmod(path, 0o644, {
recursive: true
})
await ipfs.files.chmod(bin, 'u+x')
await expect(ipfs.files.stat(path)).to.eventually.have.property('mode', 0o644)
await expect(ipfs.files.stat(sub)).to.eventually.have.property('mode', 0o644)
await expect(ipfs.files.stat(file)).to.eventually.have.property('mode', 0o644)
await expect(ipfs.files.stat(bin)).to.eventually.have.property('mode', 0o744)
await ipfs.files.chmod(path, 'a+X', {
recursive: true
})
// directories should be world-executable
await expect(ipfs.files.stat(path)).to.eventually.have.property('mode', 0o755)
await expect(ipfs.files.stat(sub)).to.eventually.have.property('mode', 0o755)
// files without prior execute bit should be untouched
await expect(ipfs.files.stat(file)).to.eventually.have.property('mode', 0o644)
// files with prior execute bit should now be world-executable
await expect(ipfs.files.stat(bin)).to.eventually.have.property('mode', 0o755)
})
it('should apply special execute permissions to user', async () => {
const path = `/foo-${Math.random()}`
const sub = `${path}/sub`
const file = `${path}/sub/foo.txt`
const bin = `${path}/sub/bar`
await ipfs.files.mkdir(sub, {
parents: true
})
await ipfs.files.touch(file)
await ipfs.files.touch(bin)
await ipfs.files.chmod(path, 0o644, {
recursive: true
})
await ipfs.files.chmod(bin, 'u+x')
await expect(ipfs.files.stat(path)).to.eventually.have.property('mode', 0o644)
await expect(ipfs.files.stat(sub)).to.eventually.have.property('mode', 0o644)
await expect(ipfs.files.stat(file)).to.eventually.have.property('mode', 0o644)
await expect(ipfs.files.stat(bin)).to.eventually.have.property('mode', 0o744)
await ipfs.files.chmod(path, 'u+X', {
recursive: true
})
// directories should be user executable
await expect(ipfs.files.stat(path)).to.eventually.have.property('mode', 0o744)
await expect(ipfs.files.stat(sub)).to.eventually.have.property('mode', 0o744)
// files without prior execute bit should be untouched
await expect(ipfs.files.stat(file)).to.eventually.have.property('mode', 0o644)
// files with prior execute bit should now be user executable
await expect(ipfs.files.stat(bin)).to.eventually.have.property('mode', 0o744)
})
it('should apply special execute permissions to user and group', async () => {
const path = `/foo-${Math.random()}`
const sub = `${path}/sub`
const file = `${path}/sub/foo.txt`
const bin = `${path}/sub/bar`
await ipfs.files.mkdir(sub, {
parents: true
})
await ipfs.files.touch(file)
await ipfs.files.touch(bin)
await ipfs.files.chmod(path, 0o644, {
recursive: true
})
await ipfs.files.chmod(bin, 'u+x')
await expect(ipfs.files.stat(path)).to.eventually.have.property('mode', 0o644)
await expect(ipfs.files.stat(sub)).to.eventually.have.property('mode', 0o644)
await expect(ipfs.files.stat(file)).to.eventually.have.property('mode', 0o644)
await expect(ipfs.files.stat(bin)).to.eventually.have.property('mode', 0o744)
await ipfs.files.chmod(path, 'ug+X', {
recursive: true
})
// directories should be user and group executable
await expect(ipfs.files.stat(path)).to.eventually.have.property('mode', 0o754)
await expect(ipfs.files.stat(sub)).to.eventually.have.property('mode', 0o754)
// files without prior execute bit should be untouched
await expect(ipfs.files.stat(file)).to.eventually.have.property('mode', 0o644)
// files with prior execute bit should now be user and group executable
await expect(ipfs.files.stat(bin)).to.eventually.have.property('mode', 0o754)
})
it('should apply special execute permissions to sharded directories', async () => {
const path = `/foo-${Math.random()}`
const sub = `${path}/sub`
const file = `${path}/sub/foo.txt`
const bin = `${path}/sub/bar`
await ipfs.files.mkdir(sub, {
parents: true,
shardSplitThreshold: 0
})
await ipfs.files.touch(file, {
shardSplitThreshold: 0
})
await ipfs.files.touch(bin, {
shardSplitThreshold: 0
})
await ipfs.files.chmod(path, 0o644, {
recursive: true,
shardSplitThreshold: 0
})
await ipfs.files.chmod(bin, 'u+x', {
recursive: true,
shardSplitThreshold: 0
})
await expect(ipfs.files.stat(path)).to.eventually.have.property('mode', 0o644)
await expect(ipfs.files.stat(sub)).to.eventually.have.property('mode', 0o644)
await expect(ipfs.files.stat(file)).to.eventually.have.property('mode', 0o644)
await expect(ipfs.files.stat(bin)).to.eventually.have.property('mode', 0o744)
await ipfs.files.chmod(path, 'ug+X', {
recursive: true,
shardSplitThreshold: 0
})
// directories should be user and group executable
await expect(isShardAtPath(path, ipfs)).to.eventually.be.true()
await expect(ipfs.files.stat(path)).to.eventually.include({
type: 'directory',
mode: 0o754
})
await expect(ipfs.files.stat(sub)).to.eventually.have.property('mode', 0o754)
// files without prior execute bit should be untouched
await expect(ipfs.files.stat(file)).to.eventually.have.property('mode', 0o644)
// files with prior execute bit should now be user and group executable
await expect(ipfs.files.stat(bin)).to.eventually.have.property('mode', 0o754)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/files/cp.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import { nanoid } from 'nanoid'
import all from 'it-all'
import { fixtures } from '../utils/index.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { identity } from 'multiformats/hashes/identity'
import { CID } from 'multiformats/cid'
import { randomBytes } from 'iso-random-stream'
import { createShardedDirectory } from '../utils/create-sharded-directory.js'
import isShardAtPath from '../utils/is-shard-at-path.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testCp (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.files.cp', function () {
this.timeout(120 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('refuses to copy files without a source', async () => {
// @ts-expect-error invalid args
await expect(ipfs.files.cp()).to.eventually.be.rejected.with('Please supply at least one source')
})
it('refuses to copy files without a source, even with options', async () => {
// @ts-expect-error invalid args
await expect(ipfs.files.cp({})).to.eventually.be.rejected.with('Please supply at least one source')
})
it('refuses to copy files without a destination', async () => {
// @ts-expect-error invalid args
await expect(ipfs.files.cp('/source')).to.eventually.be.rejected.with('Please supply at least one source')
})
it('refuses to copy files without a destination, even with options', async () => {
// @ts-expect-error invalid args
await expect(ipfs.files.cp('/source', {})).to.eventually.be.rejected.with('Please supply at least one source')
})
it('refuses to copy a non-existent file', async () => {
await expect(ipfs.files.cp('/i-do-not-exist', '/destination', {})).to.eventually.be.rejected.with('does not exist')
})
it('refuses to copy multiple files to a non-existent child directory', async () => {
const src1 = `/src1-${Math.random()}`
const src2 = `/src2-${Math.random()}`
const parent = `/output-${Math.random()}`
await ipfs.files.write(src1, [], {
create: true
})
await ipfs.files.write(src2, [], {
create: true
})
await ipfs.files.mkdir(parent)
await expect(ipfs.files.cp([src1, src2], `${parent}/child`)).to.eventually.be.rejectedWith(Error)
.that.has.property('message').that.matches(/destination did not exist/)
})
it('refuses to copy files to an unreadable node', async () => {
const src1 = `/src2-${Math.random()}`
const parent = `/output-${Math.random()}`
const hash = await identity.digest(uint8ArrayFromString('derp'))
const cid = CID.createV1(identity.code, hash)
await ipfs.block.put(uint8ArrayFromString('derp'), {
mhtype: 'identity'
})
await ipfs.files.cp(`/ipfs/${cid}`, parent)
await ipfs.files.write(src1, [], {
create: true
})
await expect(ipfs.files.cp(src1, `${parent}/child`)).to.eventually.be.rejectedWith(Error)
.that.has.property('message').that.matches(/unsupported codec/i)
})
it('refuses to copy files to an exsting file', async () => {
const source = `/source-file-${Math.random()}.txt`
const destination = `/dest-file-${Math.random()}.txt`
await ipfs.files.write(source, randomBytes(100), {
create: true
})
await ipfs.files.write(destination, randomBytes(100), {
create: true
})
try {
await ipfs.files.cp(source, destination)
throw new Error('No error was thrown when trying to overwrite a file')
} catch (/** @type {any} */ err) {
expect(err.message).to.contain('directory already has entry by that name')
}
})
it('refuses to copy a file to itself', async () => {
const source = `/source-file-${Math.random()}.txt`
await ipfs.files.write(source, randomBytes(100), {
create: true
})
try {
await ipfs.files.cp(source, source)
throw new Error('No error was thrown for a non-existent file')
} catch (/** @type {any} */ err) {
expect(err.message).to.contain('directory already has entry by that name')
}
})
it('copies a file to new location', async () => {
const source = `/source-file-${Math.random()}.txt`
const destination = `/dest-file-${Math.random()}.txt`
const data = randomBytes(500)
await ipfs.files.write(source, data, {
create: true
})
await ipfs.files.cp(source, destination)
const bytes = uint8ArrayConcat(await all(ipfs.files.read(destination)))
expect(bytes).to.deep.equal(data)
})
it('copies a file to a pre-existing directory', async () => {
const source = `/source-file-${Math.random()}.txt`
const directory = `/dest-directory-${Math.random()}`
const destination = `${directory}${source}`
await ipfs.files.write(source, randomBytes(500), {
create: true
})
await ipfs.files.mkdir(directory)
await ipfs.files.cp(source, directory)
const stats = await ipfs.files.stat(destination)
expect(stats.size).to.equal(500)
})
it('copies directories', async () => {
const source = `/source-directory-${Math.random()}`
const destination = `/dest-directory-${Math.random()}`
await ipfs.files.mkdir(source)
await ipfs.files.cp(source, destination)
const stats = await ipfs.files.stat(destination)
expect(stats.type).to.equal('directory')
})
it('copies directories recursively', async () => {
const directory = `/source-directory-${Math.random()}`
const subDirectory = `/source-directory-${Math.random()}`
const source = `${directory}${subDirectory}`
const destination = `/dest-directory-${Math.random()}`
await ipfs.files.mkdir(source, {
parents: true
})
await ipfs.files.cp(directory, destination)
const stats = await ipfs.files.stat(destination)
expect(stats.type).to.equal('directory')
const subDirStats = await ipfs.files.stat(`${destination}/${subDirectory}`)
expect(subDirStats.type).to.equal('directory')
})
it('copies multiple files to new location', async () => {
const sources = [{
path: `/source-file-${Math.random()}.txt`,
data: randomBytes(500)
}, {
path: `/source-file-${Math.random()}.txt`,
data: randomBytes(500)
}]
const destination = `/dest-dir-${Math.random()}`
for (const source of sources) {
await ipfs.files.write(source.path, source.data, {
create: true
})
}
await ipfs.files.cp([sources[0].path, sources[1].path], destination, {
parents: true
})
for (const source of sources) {
const bytes = uint8ArrayConcat(await all(ipfs.files.read(`${destination}${source.path}`)))
expect(bytes).to.deep.equal(source.data)
}
})
it('copies files from ipfs paths', async () => {
const source = `/source-file-${Math.random()}.txt`
const destination = `/dest-file-${Math.random()}.txt`
await ipfs.files.write(source, randomBytes(100), {
create: true
})
const stats = await ipfs.files.stat(source)
await ipfs.files.cp(`/ipfs/${stats.cid}`, destination)
const destinationStats = await ipfs.files.stat(destination)
expect(destinationStats.size).to.equal(100)
})
it('copies files from deep ipfs paths', async () => {
const dir = `dir-${Math.random()}`
const file = `source-file-${Math.random()}.txt`
const source = `/${dir}/${file}`
const destination = `/dest-file-${Math.random()}.txt`
await ipfs.files.write(source, randomBytes(100), {
create: true,
parents: true
})
const stats = await ipfs.files.stat(`/${dir}`)
await ipfs.files.cp(`/ipfs/${stats.cid}/${file}`, destination)
const destinationStats = await ipfs.files.stat(destination)
expect(destinationStats.size).to.equal(100)
})
it('copies files to deep mfs paths and creates intermediate directories', async () => {
const source = `/source-file-${Math.random()}.txt`
const destination = `/really/deep/path/to/dest-file-${Math.random()}.txt`
await ipfs.files.write(source, randomBytes(100), {
create: true
})
await ipfs.files.cp(source, destination, {
parents: true
})
const destinationStats = await ipfs.files.stat(destination)
expect(destinationStats.size).to.equal(100)
})
it('fails to copy files to deep mfs paths when intermediate directories do not exist', async () => {
const source = `/source-file-${Math.random()}.txt`
const destination = `/really/deep/path-${Math.random()}/to-${Math.random()}/dest-file-${Math.random()}.txt`
await ipfs.files.write(source, randomBytes(100), {
create: true
})
await expect(ipfs.files.cp(source, destination)).to.eventually.be.rejected()
})
it('should respect metadata when copying files', async function () {
const testSrcPath = `/test-${nanoid()}`
const testDestPath = `/test-${nanoid()}`
const mode = parseInt('0321', 8)
const mtime = new Date()
const seconds = Math.floor(mtime.getTime() / 1000)
const expectedMtime = {
secs: seconds,
nsecs: (mtime.getTime() - (seconds * 1000)) * 1000
}
await ipfs.files.write(testSrcPath, uint8ArrayFromString('TEST'), {
create: true,
mode,
mtime
})
await ipfs.files.cp(testSrcPath, testDestPath)
const stats = await ipfs.files.stat(testDestPath)
expect(stats).to.have.deep.property('mtime', expectedMtime)
expect(stats).to.have.property('mode', mode)
})
it('should respect metadata when copying directories', async function () {
const testSrcPath = `/test-${nanoid()}`
const testDestPath = `/test-${nanoid()}`
const mode = parseInt('0321', 8)
const mtime = new Date()
const seconds = Math.floor(mtime.getTime() / 1000)
const expectedMtime = {
secs: seconds,
nsecs: (mtime.getTime() - (seconds * 1000)) * 1000
}
await ipfs.files.mkdir(testSrcPath, {
mode,
mtime
})
await ipfs.files.cp(testSrcPath, testDestPath, {
recursive: true
})
const stats = await ipfs.files.stat(testDestPath)
expect(stats).to.have.deep.property('mtime', expectedMtime)
expect(stats).to.have.property('mode', mode)
})
it('should respect metadata when copying from outside of mfs', async function () {
const testDestPath = `/test-${nanoid()}`
const mode = parseInt('0321', 8)
const mtime = new Date()
const seconds = Math.floor(mtime.getTime() / 1000)
const expectedMtime = {
secs: seconds,
nsecs: (mtime.getTime() - (seconds * 1000)) * 1000
}
const {
cid
} = await ipfs.add({
content: fixtures.smallFile.data,
mode,
mtime
})
await ipfs.files.cp(`/ipfs/${cid}`, testDestPath)
const stats = await ipfs.files.stat(testDestPath)
expect(stats).to.have.deep.property('mtime', expectedMtime)
expect(stats).to.have.property('mode', mode)
})
describe('with sharding', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async function () {
const ipfsd = await factory.spawn({
ipfsOptions: {
EXPERIMENTAL: {
// enable sharding for js
sharding: true
},
config: {
// enable sharding for go with automatic threshold dropped to the minimum so it shards everything
Internal: {
UnixFSShardingSizeThreshold: '1B'
}
}
}
})
ipfs = ipfsd.api
})
it('copies a sharded directory to a normal directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const normalDir = `dir-${Math.random()}`
const normalDirPath = `/${normalDir}`
await ipfs.files.mkdir(normalDirPath)
await ipfs.files.cp(shardedDirPath, normalDirPath)
const finalShardedDirPath = `${normalDirPath}${shardedDirPath}`
// should still be a sharded directory
await expect(isShardAtPath(finalShardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(finalShardedDirPath)).type).to.equal('directory')
const files = await all(ipfs.files.ls(finalShardedDirPath))
expect(files.length).to.be.ok()
})
it('copies a normal directory to a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const normalDir = `dir-${Math.random()}`
const normalDirPath = `/${normalDir}`
await ipfs.files.mkdir(normalDirPath)
await ipfs.files.cp(normalDirPath, shardedDirPath)
const finalDirPath = `${shardedDirPath}${normalDirPath}`
// should still be a sharded directory
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory')
expect((await ipfs.files.stat(finalDirPath)).type).to.equal('directory')
})
it('copies a file from a normal directory to a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const file = `file-${Math.random()}.txt`
const filePath = `/${file}`
const finalFilePath = `${shardedDirPath}/${file}`
await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3]), {
create: true
})
await ipfs.files.cp(filePath, finalFilePath)
// should still be a sharded directory
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory')
expect((await ipfs.files.stat(finalFilePath)).type).to.equal('file')
})
it('copies a file from a sharded directory to a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const othershardedDirPath = await createShardedDirectory(ipfs)
const file = `file-${Math.random()}.txt`
const filePath = `${shardedDirPath}/${file}`
const finalFilePath = `${othershardedDirPath}/${file}`
await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3]), {
create: true
})
await ipfs.files.cp(filePath, finalFilePath)
// should still be a sharded directory
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory')
await expect(isShardAtPath(othershardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(othershardedDirPath)).type).to.equal('directory')
expect((await ipfs.files.stat(finalFilePath)).type).to.equal('file')
})
it('copies a file from a sharded directory to a normal directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const dir = `dir-${Math.random()}`
const dirPath = `/${dir}`
const file = `file-${Math.random()}.txt`
const filePath = `${shardedDirPath}/${file}`
const finalFilePath = `${dirPath}/${file}`
await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3]), {
create: true
})
await ipfs.files.mkdir(dirPath)
await ipfs.files.cp(filePath, finalFilePath)
// should still be a sharded directory
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory')
expect((await ipfs.files.stat(dirPath)).type).to.equal('directory')
expect((await ipfs.files.stat(finalFilePath)).type).to.equal('file')
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/files/flush.js
================================================
/* eslint-env mocha */
import { nanoid } from 'nanoid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testFlush (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.files.flush', function () {
this.timeout(120 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('should not flush not found file/dir, expect error', async () => {
const testDir = `/test-${nanoid()}`
try {
await ipfs.files.flush(`${testDir}/404`)
} catch (/** @type {any} */ err) {
expect(err).to.exist()
}
})
it('should require a path', () => {
// @ts-expect-error invalid args
expect(ipfs.files.flush()).to.eventually.be.rejected()
})
it('should flush root', async () => {
const root = await ipfs.files.stat('/')
const flushed = await ipfs.files.flush('/')
expect(root.cid.toString()).to.equal(flushed.toString())
})
it('should flush specific dir', async () => {
const testDir = `/test-${nanoid()}`
await ipfs.files.mkdir(testDir, { parents: true })
const dirStats = await ipfs.files.stat(testDir)
const flushed = await ipfs.files.flush(testDir)
expect(dirStats.cid.toString()).to.equal(flushed.toString())
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/files/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testChmod } from './chmod.js'
import { testCp } from './cp.js'
import { testFlush } from './flush.js'
import { testLs } from './ls.js'
import { testMkdir } from './mkdir.js'
import { testMv } from './mv.js'
import { testRead } from './read.js'
import { testRm } from './rm.js'
import { testStat } from './stat.js'
import { testTouch } from './touch.js'
import { testWrite } from './write.js'
const tests = {
chmod: testChmod,
cp: testCp,
flush: testFlush,
ls: testLs,
mkdir: testMkdir,
mv: testMv,
read: testRead,
rm: testRm,
stat: testStat,
touch: testTouch,
write: testWrite
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/files/ls.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { CID } from 'multiformats/cid'
import { createShardedDirectory } from '../utils/create-sharded-directory.js'
import all from 'it-all'
import { randomBytes } from 'iso-random-stream'
import * as raw from 'multiformats/codecs/raw'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testLs (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
const largeFile = randomBytes(490668)
describe('.files.ls', function () {
this.timeout(120 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('should require a path', () => {
// @ts-expect-error invalid args
expect(all(ipfs.files.ls())).to.eventually.be.rejected()
})
it('lists the root directory', async () => {
const fileName = `small-file-${Math.random()}.txt`
const content = uint8ArrayFromString('Hello world')
await ipfs.files.write(`/${fileName}`, content, {
create: true
})
const files = await all(ipfs.files.ls('/'))
expect(files).to.have.lengthOf(1).and.to.containSubset([{
cid: CID.parse('Qmetpc7cZmN25Wcc6R27cGCAvCDqCS5GjHG4v7xABEfpmJ'),
name: fileName,
size: content.length,
type: 'file'
}])
})
it('refuses to lists files with an empty path', async () => {
await expect(all(ipfs.files.ls(''))).to.eventually.be.rejected()
})
it('refuses to lists files with an invalid path', async () => {
await expect(all(ipfs.files.ls('not-valid'))).to.eventually.be.rejected()
})
it('lists files in a directory', async () => {
const dirName = `dir-${Math.random()}`
const fileName = `small-file-${Math.random()}.txt`
const content = uint8ArrayFromString('Hello world')
await ipfs.files.write(`/${dirName}/${fileName}`, content, {
create: true,
parents: true
})
const files = await all(ipfs.files.ls(`/${dirName}`))
expect(files).to.have.lengthOf(1).and.to.containSubset([{
cid: CID.parse('Qmetpc7cZmN25Wcc6R27cGCAvCDqCS5GjHG4v7xABEfpmJ'),
name: fileName,
size: content.length,
type: 'file'
}])
})
it('lists a file', async () => {
const fileName = `small-file-${Math.random()}.txt`
const content = uint8ArrayFromString('Hello world')
await ipfs.files.write(`/${fileName}`, content, {
create: true
})
const files = await all(ipfs.files.ls(`/${fileName}`))
expect(files).to.have.lengthOf(1).and.to.containSubset([{
cid: CID.parse('Qmetpc7cZmN25Wcc6R27cGCAvCDqCS5GjHG4v7xABEfpmJ'),
name: fileName,
size: content.length,
type: 'file'
}])
})
it('fails to list non-existent file', async () => {
await expect(all(ipfs.files.ls('/i-do-not-exist'))).to.eventually.be.rejected()
})
it('lists a raw node', async () => {
const filePath = '/stat/large-file.txt'
await ipfs.files.write(filePath, largeFile, {
create: true,
parents: true,
rawLeaves: true
})
const stats = await ipfs.files.stat(filePath)
const { value: node } = await ipfs.dag.get(stats.cid)
expect(node).to.have.nested.property('Links[0].Hash.code', raw.code)
const child = node.Links[0]
const files = await all(ipfs.files.ls(`/ipfs/${child.Hash}`))
expect(files).to.have.lengthOf(1).and.to.containSubset([{
cid: child.Hash,
name: child.Hash.toString(),
size: 262144,
type: 'file'
}])
})
it('lists a raw node in an mfs directory', async () => {
const filePath = '/stat/large-file.txt'
await ipfs.files.write(filePath, largeFile, {
create: true,
parents: true,
rawLeaves: true
})
const stats = await ipfs.files.stat(filePath)
const cid = stats.cid
const { value: node } = await ipfs.dag.get(cid)
expect(node).to.have.nested.property('Links[0].Hash.code', raw.code)
const child = node.Links[0]
const dir = `/dir-with-raw-${Math.random()}`
const path = `${dir}/raw-${Math.random()}`
await ipfs.files.mkdir(dir)
await ipfs.files.cp(`/ipfs/${child.Hash}`, path)
const files = await all(ipfs.files.ls(`/ipfs/${child.Hash}`))
expect(files).to.have.lengthOf(1).and.to.containSubset([{
cid: child.Hash,
name: child.Hash.toString(),
size: 262144,
type: 'file'
}])
})
describe('with sharding', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async function () {
const ipfsd = await factory.spawn({
ipfsOptions: {
EXPERIMENTAL: {
// enable sharding for js
sharding: true
},
config: {
// enable sharding for go with automatic threshold dropped to the minimum so it shards everything
Internal: {
UnixFSShardingSizeThreshold: '1B'
}
}
}
})
ipfs = ipfsd.api
})
it('lists a sharded directory contents', async () => {
const fileCount = 1001
const dirPath = await createShardedDirectory(ipfs, fileCount)
const files = await all(ipfs.files.ls(dirPath))
expect(files.length).to.equal(fileCount)
files.forEach(file => {
// should be a file
expect(file.type).to.equal('file')
})
})
it('lists a file inside a sharded directory directly', async () => {
const dirPath = await createShardedDirectory(ipfs)
const files = await all(ipfs.files.ls(dirPath))
const filePath = `${dirPath}/${files[0].name}`
// should be able to ls new file directly
const file = await all(ipfs.files.ls(filePath))
expect(file).to.have.lengthOf(1).and.to.containSubset([files[0]])
})
it('lists the contents of a directory inside a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const dirPath = `${shardedDirPath}/subdir-${Math.random()}`
const fileName = `small-file-${Math.random()}.txt`
await ipfs.files.mkdir(`${dirPath}`)
await ipfs.files.write(`${dirPath}/${fileName}`, Uint8Array.from([0, 1, 2, 3]), {
create: true
})
const files = await all(ipfs.files.ls(dirPath))
expect(files.length).to.equal(1)
expect(files.filter(file => file.name === fileName)).to.be.ok()
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/files/mkdir.js
================================================
/* eslint-env mocha */
import { nanoid } from 'nanoid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { sha512 } from 'multiformats/hashes/sha2'
import { createShardedDirectory } from '../utils/create-sharded-directory.js'
import all from 'it-all'
import isShardAtPath from '../utils/is-shard-at-path.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testMkdir (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.files.mkdir', function () {
this.timeout(120 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/**
* @param {number | string | undefined} mode
* @param {number} expectedMode
*/
async function testMode (mode, expectedMode) {
const testPath = `/test-${nanoid()}`
await ipfs.files.mkdir(testPath, {
mode
})
const stats = await ipfs.files.stat(testPath)
expect(stats).to.have.property('mode', expectedMode)
}
/**
* @param {import('ipfs-unixfs').MtimeLike} mtime
* @param {import('ipfs-unixfs').MtimeLike} expectedMtime
*/
async function testMtime (mtime, expectedMtime) {
const testPath = `/test-${nanoid()}`
await ipfs.files.mkdir(testPath, {
mtime
})
const stats = await ipfs.files.stat(testPath)
expect(stats).to.have.deep.property('mtime', expectedMtime)
}
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('requires a directory', async () => {
await expect(ipfs.files.mkdir('')).to.eventually.be.rejected()
})
it('refuses to create a directory without a leading slash', async () => {
await expect(ipfs.files.mkdir('foo')).to.eventually.be.rejected()
})
it('refuses to recreate the root directory when -p is false', async () => {
await expect(ipfs.files.mkdir('/', {
parents: false
})).to.eventually.be.rejected()
})
it('refuses to create a nested directory when -p is false', async () => {
await expect(ipfs.files.mkdir('/foo/bar/baz', {
parents: false
})).to.eventually.be.rejected()
})
it('creates a directory', async () => {
const path = '/foo'
await ipfs.files.mkdir(path, {})
const stats = await ipfs.files.stat(path)
expect(stats.type).to.equal('directory')
const files = await all(ipfs.files.ls(path))
expect(files.length).to.equal(0)
})
it('refuses to create a directory that already exists', async () => {
const path = '/qux/quux/quuux'
await ipfs.files.mkdir(path, {
parents: true
})
await expect(ipfs.files.mkdir(path, {
parents: false
})).to.eventually.be.rejected()
})
it('does not error when creating a directory that already exists and parents is true', async () => {
const path = '/qux/quux/quuux'
await ipfs.files.mkdir(path, {
parents: true
})
await ipfs.files.mkdir(path, {
parents: true
})
})
it('creates a nested directory when -p is true', async () => {
const path = '/foo/bar/baz'
await ipfs.files.mkdir(path, {
parents: true
})
const files = await all(ipfs.files.ls(path))
expect(files.length).to.equal(0)
})
it('creates nested directories', async () => {
await ipfs.files.mkdir('/nested-dir')
await ipfs.files.mkdir('/nested-dir/baz')
const files = await all(ipfs.files.ls('/nested-dir'))
expect(files.length).to.equal(1)
})
it('creates a nested directory with a different CID version to the parent', async () => {
const directory = `cid-versions-${Math.random()}`
const directoryPath = `/${directory}`
const subDirectory = `cid-versions-${Math.random()}`
const subDirectoryPath = `${directoryPath}/${subDirectory}`
await ipfs.files.mkdir(directoryPath, {
cidVersion: 0
})
await expect(ipfs.files.stat(directoryPath)).to.eventually.have.nested.property('cid.version', 0)
await ipfs.files.mkdir(subDirectoryPath, {
cidVersion: 1
})
await expect(ipfs.files.stat(subDirectoryPath)).to.eventually.have.nested.property('cid.version', 1)
})
it('creates a nested directory with a different hash function to the parent', async () => {
const directory = `cid-versions-${Math.random()}`
const directoryPath = `/${directory}`
const subDirectory = `cid-versions-${Math.random()}`
const subDirectoryPath = `${directoryPath}/${subDirectory}`
await ipfs.files.mkdir(directoryPath, {
cidVersion: 0
})
await expect(ipfs.files.stat(directoryPath)).to.eventually.have.nested.property('cid.version', 0)
await ipfs.files.mkdir(subDirectoryPath, {
cidVersion: 1,
hashAlg: 'sha2-512'
})
await expect(ipfs.files.stat(subDirectoryPath)).to.eventually.have.nested.property('cid.multihash.code', sha512.code)
})
it('should make directory and have default mode', async function () {
await testMode(undefined, parseInt('0755', 8))
})
it('should make directory and specify mode as string', async function () {
const mode = '0321'
await testMode(mode, parseInt(mode, 8))
})
it('should make directory and specify mode as number', async function () {
const mode = parseInt('0321', 8)
await testMode(mode, mode)
})
it('should make directory and specify mtime as Date', async function () {
const mtime = new Date(5000)
await testMtime(mtime, {
secs: 5,
nsecs: 0
})
})
it('should make directory and specify mtime as { nsecs, secs }', async function () {
const mtime = {
secs: 5,
nsecs: 0
}
await testMtime(mtime, mtime)
})
it('should make directory and specify mtime as timespec', async function () {
await testMtime({
Seconds: 5,
FractionalNanoseconds: 0
}, {
secs: 5,
nsecs: 0
})
})
it('should make directory and specify mtime as hrtime', async function () {
const mtime = process.hrtime()
await testMtime(mtime, {
secs: mtime[0],
nsecs: mtime[1]
})
})
describe('with sharding', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async function () {
const ipfsd = await factory.spawn({
ipfsOptions: {
EXPERIMENTAL: {
// enable sharding for js
sharding: true
},
config: {
// enable sharding for go with automatic threshold dropped to the minimum so it shards everything
Internal: {
UnixFSShardingSizeThreshold: '1B'
}
}
}
})
ipfs = ipfsd.api
})
it('makes a directory inside a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const dirPath = `${shardedDirPath}/subdir-${Math.random()}`
await ipfs.files.mkdir(`${dirPath}`)
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
await expect(ipfs.files.stat(shardedDirPath)).to.eventually.have.property('type', 'directory')
await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.false()
await expect(ipfs.files.stat(dirPath)).to.eventually.have.property('type', 'directory')
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/files/mv.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { createShardedDirectory } from '../utils/create-sharded-directory.js'
import { randomBytes } from 'iso-random-stream'
import isShardAtPath from '../utils/is-shard-at-path.js'
import all from 'it-all'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testMv (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.files.mv', function () {
this.timeout(120 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
before(async () => {
await ipfs.files.mkdir('/test/lv1/lv2', { parents: true })
await ipfs.files.write('/test/a', uint8ArrayFromString('Hello, world!'), { create: true })
})
after(() => factory.clean())
it('refuses to move files without arguments', async () => {
// @ts-expect-error invalid args
await expect(ipfs.files.mv()).to.eventually.be.rejected()
})
it('refuses to move files without enough arguments', async () => {
// @ts-expect-error invalid args
await expect(ipfs.files.mv()).to.eventually.be.rejected()
})
it('moves a file', async () => {
const source = `/source-file-${Math.random()}.txt`
const destination = `/dest-file-${Math.random()}.txt`
const data = randomBytes(500)
await ipfs.files.write(source, data, {
create: true
})
await ipfs.files.mv(source, destination)
const bytes = uint8ArrayConcat(await all(ipfs.files.read(destination)))
expect(bytes).to.deep.equal(data)
await expect(ipfs.files.stat(source)).to.eventually.be.rejectedWith(/does not exist/)
})
it('moves a directory', async () => {
const source = `/source-directory-${Math.random()}`
const destination = `/dest-directory-${Math.random()}`
await ipfs.files.mkdir(source)
await ipfs.files.mv(source, destination, {
recursive: true
})
const stats = await ipfs.files.stat(destination)
expect(stats.type).to.equal('directory')
try {
await ipfs.files.stat(source)
throw new Error('Directory was copied but not removed')
} catch (/** @type {any} */ err) {
expect(err.message).to.contain('does not exist')
}
})
it('moves directories recursively', async () => {
const directory = `source-directory-${Math.random()}`
const subDirectory = `/source-directory-${Math.random()}`
const source = `/${directory}${subDirectory}`
const destination = `/dest-directory-${Math.random()}`
await ipfs.files.mkdir(source, {
parents: true
})
await ipfs.files.mv(`/${directory}`, destination, {
recursive: true
})
const stats = await ipfs.files.stat(destination)
expect(stats.type).to.equal('directory')
const subDirectoryStats = await ipfs.files.stat(`${destination}${subDirectory}`)
expect(subDirectoryStats.type).to.equal('directory')
try {
await ipfs.files.stat(source)
throw new Error('Directory was copied but not removed')
} catch (/** @type {any} */ err) {
expect(err.message).to.contain('does not exist')
}
})
describe('with sharding', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async function () {
const ipfsd = await factory.spawn({
ipfsOptions: {
EXPERIMENTAL: {
// enable sharding for js
sharding: true
},
config: {
// enable sharding for go with automatic threshold dropped to the minimum so it shards everything
Internal: {
UnixFSShardingSizeThreshold: '1B'
}
}
}
})
ipfs = ipfsd.api
})
it('moves a sharded directory to a normal directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const dirPath = `/dir-${Math.random()}`
const finalShardedDirPath = `${dirPath}${shardedDirPath}`
await ipfs.files.mkdir(dirPath)
await ipfs.files.mv(shardedDirPath, dirPath)
await expect(isShardAtPath(finalShardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(finalShardedDirPath)).type).to.equal('directory')
expect((await ipfs.files.stat(dirPath)).type).to.equal('directory')
try {
await ipfs.files.stat(shardedDirPath)
throw new Error('Dir was not removed')
} catch (/** @type {any} */ error) {
expect(error.message).to.contain('does not exist')
}
})
it('moves a normal directory to a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const dirPath = `/dir-${Math.random()}`
const finalDirPath = `${shardedDirPath}${dirPath}`
await ipfs.files.mkdir(dirPath)
await ipfs.files.mv(dirPath, shardedDirPath)
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory')
expect((await ipfs.files.stat(finalDirPath)).type).to.equal('directory')
try {
await ipfs.files.stat(dirPath)
throw new Error('Dir was not removed')
} catch (/** @type {any} */ error) {
expect(error.message).to.contain('does not exist')
}
})
it('moves a sharded directory to a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const otherShardedDirPath = await createShardedDirectory(ipfs)
const finalShardedDirPath = `${shardedDirPath}${otherShardedDirPath}`
await ipfs.files.mv(otherShardedDirPath, shardedDirPath)
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory')
await expect(isShardAtPath(finalShardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(finalShardedDirPath)).type).to.equal('directory')
try {
await ipfs.files.stat(otherShardedDirPath)
throw new Error('Sharded dir was not removed')
} catch (/** @type {any} */ error) {
expect(error.message).to.contain('does not exist')
}
})
it('moves a file from a normal directory to a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const dirPath = `/dir-${Math.random()}`
const file = `file-${Math.random()}.txt`
const filePath = `${dirPath}/${file}`
const finalFilePath = `${shardedDirPath}/${file}`
await ipfs.files.mkdir(dirPath)
await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3, 4]), {
create: true
})
await ipfs.files.mv(filePath, shardedDirPath)
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory')
expect((await ipfs.files.stat(finalFilePath)).type).to.equal('file')
try {
await ipfs.files.stat(filePath)
throw new Error('File was not removed')
} catch (/** @type {any} */ error) {
expect(error.message).to.contain('does not exist')
}
})
it('moves a file from a sharded directory to a normal directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const dirPath = `/dir-${Math.random()}`
const file = `file-${Math.random()}.txt`
const filePath = `${shardedDirPath}/${file}`
const finalFilePath = `${dirPath}/${file}`
await ipfs.files.mkdir(dirPath)
await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3, 4]), {
create: true
})
await ipfs.files.mv(filePath, dirPath)
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory')
expect((await ipfs.files.stat(finalFilePath)).type).to.equal('file')
expect((await ipfs.files.stat(dirPath)).type).to.equal('directory')
try {
await ipfs.files.stat(filePath)
throw new Error('File was not removed')
} catch (/** @type {any} */ error) {
expect(error.message).to.contain('does not exist')
}
})
it('moves a file from a sharded directory to a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const otherShardedDirPath = await createShardedDirectory(ipfs)
const file = `file-${Math.random()}.txt`
const filePath = `${shardedDirPath}/${file}`
const finalFilePath = `${otherShardedDirPath}/${file}`
await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3, 4]), {
create: true
})
await ipfs.files.mv(filePath, otherShardedDirPath)
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory')
expect((await ipfs.files.stat(finalFilePath)).type).to.equal('file')
await expect(isShardAtPath(otherShardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(otherShardedDirPath)).type).to.equal('directory')
try {
await ipfs.files.stat(filePath)
throw new Error('File was not removed')
} catch (/** @type {any} */ error) {
expect(error.message).to.contain('does not exist')
}
})
it('moves a file from a sub-shard of a sharded directory to a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const otherShardedDirPath = await createShardedDirectory(ipfs)
const file = 'file-1a.txt'
const filePath = `${shardedDirPath}/${file}`
const finalFilePath = `${otherShardedDirPath}/${file}`
await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3, 4]), {
create: true
})
await ipfs.files.mv(filePath, otherShardedDirPath)
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory')
expect((await ipfs.files.stat(finalFilePath)).type).to.equal('file')
await expect(isShardAtPath(otherShardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(otherShardedDirPath)).type).to.equal('directory')
try {
await ipfs.files.stat(filePath)
throw new Error('File was not removed')
} catch (/** @type {any} */ error) {
expect(error.message).to.contain('does not exist')
}
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/files/read.js
================================================
/* eslint-env mocha */
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import drain from 'it-drain'
import all from 'it-all'
import { fixtures } from '../utils/index.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { createShardedDirectory } from '../utils/create-sharded-directory.js'
import { randomBytes } from 'iso-random-stream'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testRead (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
const smallFile = randomBytes(13)
describe('.files.read', function () {
this.timeout(120 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('reads a small file', async () => {
const filePath = '/small-file.txt'
await ipfs.files.write(filePath, smallFile, {
create: true
})
const bytes = uint8ArrayConcat(await all(ipfs.files.read(filePath)))
expect(bytes).to.deep.equal(smallFile)
})
it('reads a file with an offset', async () => {
const path = `/some-file-${Math.random()}.txt`
const data = randomBytes(100)
const offset = 10
await ipfs.files.write(path, data, {
create: true
})
const bytes = uint8ArrayConcat(await all(ipfs.files.read(path, {
offset
})))
expect(bytes).to.deep.equal(data.slice(offset))
})
it('reads a file with a length', async () => {
const path = `/some-file-${Math.random()}.txt`
const data = randomBytes(100)
const length = 10
await ipfs.files.write(path, data, {
create: true
})
const bytes = uint8ArrayConcat(await all(ipfs.files.read(path, {
length
})))
expect(bytes).to.deep.equal(data.slice(0, length))
})
it('reads a file with an offset and a length', async () => {
const path = `/some-file-${Math.random()}.txt`
const data = randomBytes(100)
const offset = 10
const length = 10
await ipfs.files.write(path, data, {
create: true
})
const buffer = uint8ArrayConcat(await all(ipfs.files.read(path, {
offset,
length
})))
expect(buffer).to.deep.equal(data.slice(offset, offset + length))
})
it('refuses to read a directory', async () => {
const path = '/'
await expect(drain(ipfs.files.read(path))).to.eventually.be.rejectedWith(/not a file/)
})
it('refuses to read a non-existent file', async () => {
const path = `/file-${Math.random()}.txt`
await expect(drain(ipfs.files.read(path))).to.eventually.be.rejectedWith(/does not exist/)
})
it('should read from outside of mfs', async () => {
const { cid } = await ipfs.add(fixtures.smallFile.data)
const testFileData = uint8ArrayConcat(await all(ipfs.files.read(`/ipfs/${cid}`)))
expect(testFileData).to.eql(fixtures.smallFile.data)
})
it('should be able to read rawLeaves files', async () => {
const { cid } = await ipfs.add(fixtures.smallFile.data, {
rawLeaves: true
})
await ipfs.files.cp(`/ipfs/${cid}`, '/raw-leaves.txt')
const testFileData = uint8ArrayConcat(await all(ipfs.files.read('/raw-leaves.txt')))
expect(testFileData).to.eql(fixtures.smallFile.data)
})
describe('with sharding', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async function () {
const ipfsd = await factory.spawn({
ipfsOptions: {
EXPERIMENTAL: {
// enable sharding for js
sharding: true
},
config: {
// enable sharding for go with automatic threshold dropped to the minimum so it shards everything
Internal: {
UnixFSShardingSizeThreshold: '1B'
}
}
}
})
ipfs = ipfsd.api
})
it('reads file from inside a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const filePath = `${shardedDirPath}/file-${Math.random()}.txt`
const content = Uint8Array.from([0, 1, 2, 3, 4])
await ipfs.files.write(filePath, content, {
create: true
})
const bytes = uint8ArrayConcat(await all(ipfs.files.read(filePath)))
expect(bytes).to.deep.equal(content)
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/files/rm.js
================================================
/* eslint-env mocha */
import { nanoid } from 'nanoid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { createShardedDirectory } from '../utils/create-sharded-directory.js'
import { createTwoShards } from '../utils/create-two-shards.js'
import { randomBytes } from 'iso-random-stream'
import isShardAtPath from '../utils/is-shard-at-path.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testRm (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.files.rm', function () {
this.timeout(300 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('should not remove not found file/dir, expect error', () => {
const testDir = `/test-${nanoid()}`
return expect(ipfs.files.rm(`${testDir}/a`)).to.eventually.be.rejected()
})
it('refuses to remove files without arguments', async () => {
// @ts-expect-error invalid args
await expect(ipfs.files.rm()).to.eventually.be.rejected()
})
it('refuses to remove the root path', async () => {
await expect(ipfs.files.rm('/')).to.eventually.be.rejected()
})
it('refuses to remove a directory without the recursive flag', async () => {
const path = `/directory-${Math.random()}.txt`
await ipfs.files.mkdir(path)
await expect(ipfs.files.rm(path)).to.eventually.be.rejectedWith(/use -r to remove directories/)
})
it('refuses to remove a non-existent file', async () => {
await expect(ipfs.files.rm(`/file-${Math.random()}`)).to.eventually.be.rejectedWith(/does not exist/)
})
it('removes a file', async () => {
const file = `/some-file-${Math.random()}.txt`
await ipfs.files.write(file, randomBytes(100), {
create: true,
parents: true
})
await ipfs.files.rm(file)
await expect(ipfs.files.stat(file)).to.eventually.be.rejectedWith(/does not exist/)
})
it('removes multiple files', async () => {
const file1 = `/some-file-${Math.random()}.txt`
const file2 = `/some-file-${Math.random()}.txt`
await ipfs.files.write(file1, randomBytes(100), {
create: true,
parents: true
})
await ipfs.files.write(file2, randomBytes(100), {
create: true,
parents: true
})
await ipfs.files.rm([file1, file2])
await expect(ipfs.files.stat(file1)).to.eventually.be.rejectedWith(/does not exist/)
await expect(ipfs.files.stat(file2)).to.eventually.be.rejectedWith(/does not exist/)
})
it('removes a directory', async () => {
const directory = `/directory-${Math.random()}`
await ipfs.files.mkdir(directory)
await ipfs.files.rm(directory, {
recursive: true
})
await expect(ipfs.files.stat(directory)).to.eventually.be.rejectedWith(/does not exist/)
})
it('recursively removes a directory', async () => {
const directory = `/directory-${Math.random()}`
const subdirectory = `/directory-${Math.random()}`
const path = `${directory}${subdirectory}`
await ipfs.files.mkdir(path, {
parents: true
})
await ipfs.files.rm(directory, {
recursive: true
})
await expect(ipfs.files.stat(path)).to.eventually.be.rejectedWith(/does not exist/)
await expect(ipfs.files.stat(directory)).to.eventually.be.rejectedWith(/does not exist/)
})
it('recursively removes a directory with files in', async () => {
const directory = `/directory-${Math.random()}`
const file = `${directory}/some-file-${Math.random()}.txt`
await ipfs.files.write(file, randomBytes(100), {
create: true,
parents: true
})
await ipfs.files.rm(directory, {
recursive: true
})
await expect(ipfs.files.stat(file)).to.eventually.be.rejectedWith(/does not exist/)
await expect(ipfs.files.stat(directory)).to.eventually.be.rejectedWith(/does not exist/)
})
describe('with sharding', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async function () {
const ipfsd = await factory.spawn({
ipfsOptions: {
EXPERIMENTAL: {
// enable sharding for js
sharding: true
},
config: {
// enable sharding for go with automatic threshold dropped to the minimum so it shards everything
Internal: {
UnixFSShardingSizeThreshold: '1B'
}
}
}
})
ipfs = ipfsd.api
})
it('recursively removes a sharded directory inside a normal directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const dir = `dir-${Math.random()}`
const dirPath = `/${dir}`
await ipfs.files.mkdir(dirPath)
await ipfs.files.mv(shardedDirPath, dirPath)
const finalShardedDirPath = `${dirPath}${shardedDirPath}`
await expect(isShardAtPath(finalShardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(finalShardedDirPath)).type).to.equal('directory')
await ipfs.files.rm(dirPath, {
recursive: true
})
await expect(ipfs.files.stat(dirPath)).to.eventually.be.rejectedWith(/does not exist/)
await expect(ipfs.files.stat(shardedDirPath)).to.eventually.be.rejectedWith(/does not exist/)
})
it('recursively removes a sharded directory inside a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const otherDirPath = await createShardedDirectory(ipfs)
await ipfs.files.mv(shardedDirPath, otherDirPath)
const finalShardedDirPath = `${otherDirPath}${shardedDirPath}`
await expect(isShardAtPath(finalShardedDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(finalShardedDirPath)).type).to.equal('directory')
await expect(isShardAtPath(otherDirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(otherDirPath)).type).to.equal('directory')
await ipfs.files.rm(otherDirPath, {
recursive: true
})
await expect(ipfs.files.stat(otherDirPath)).to.eventually.be.rejectedWith(/does not exist/)
await expect(ipfs.files.stat(finalShardedDirPath)).to.eventually.be.rejectedWith(/does not exist/)
})
})
it('results in the same hash as a sharded directory created by the importer when removing a file', async function () {
const {
nextFile,
dirWithAllFiles,
dirWithSomeFiles,
dirPath
} = await createTwoShards(ipfs, 1001)
await ipfs.files.cp(`/ipfs/${dirWithAllFiles}`, dirPath)
await ipfs.files.rm(nextFile.path)
const stats = await ipfs.files.stat(dirPath)
const updatedDirCid = stats.cid
await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(dirPath)).type).to.equal('directory')
expect(updatedDirCid.toString()).to.deep.equal(dirWithSomeFiles.toString())
})
it('results in the same hash as a sharded directory created by the importer when removing a subshard', async function () {
const {
nextFile,
dirWithAllFiles,
dirWithSomeFiles,
dirPath
} = await createTwoShards(ipfs, 31)
await ipfs.files.cp(`/ipfs/${dirWithAllFiles}`, dirPath)
await ipfs.files.rm(nextFile.path)
const stats = await ipfs.files.stat(dirPath)
const updatedDirCid = stats.cid
await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(dirPath)).type).to.equal('directory')
expect(updatedDirCid.toString()).to.deep.equal(dirWithSomeFiles.toString())
})
it('results in the same hash as a sharded directory created by the importer when removing a file from a subshard of a subshard', async function () {
const {
nextFile,
dirWithAllFiles,
dirWithSomeFiles,
dirPath
} = await createTwoShards(ipfs, 2187)
await ipfs.files.cp(`/ipfs/${dirWithAllFiles}`, dirPath)
await ipfs.files.rm(nextFile.path)
const stats = await ipfs.files.stat(dirPath)
const updatedDirCid = stats.cid
await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(dirPath)).type).to.equal('directory')
expect(updatedDirCid.toString()).to.deep.equal(dirWithSomeFiles.toString())
})
it('results in the same hash as a sharded directory created by the importer when removing a subshard of a subshard', async function () {
const {
nextFile,
dirWithAllFiles,
dirWithSomeFiles,
dirPath
} = await createTwoShards(ipfs, 139)
await ipfs.files.cp(`/ipfs/${dirWithAllFiles}`, dirPath)
await ipfs.files.rm(nextFile.path)
const stats = await ipfs.files.stat(dirPath)
const updatedDirCid = stats.cid
await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true()
expect((await ipfs.files.stat(dirPath)).type).to.equal('directory')
expect(updatedDirCid.toString()).to.deep.equal(dirWithSomeFiles.toString())
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/files/stat.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { nanoid } from 'nanoid'
import { fixtures } from '../utils/index.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { createShardedDirectory } from '../utils/create-sharded-directory.js'
import { CID } from 'multiformats/cid'
import { identity } from 'multiformats/hashes/identity'
import { randomBytes } from 'iso-random-stream'
import isShardAtPath from '../utils/is-shard-at-path.js'
import * as raw from 'multiformats/codecs/raw'
import { isBrowser } from 'wherearewe'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testStat (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
const smallFile = randomBytes(13)
const largeFile = randomBytes(490668)
describe('.files.stat', function () {
this.timeout(120 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn({
args: factory.opts.type === 'go' ? [] : ['--enable-sharding-experiment']
})).api
})
before(async () => { await ipfs.add(fixtures.smallFile.data) })
after(() => factory.clean())
it('refuses to stat files with an empty path', async () => {
await expect(ipfs.files.stat('')).to.eventually.be.rejected()
})
it('refuses to lists files with an invalid path', async () => {
await expect(ipfs.files.stat('not-valid')).to.eventually.be.rejectedWith(/paths must start with a leading slash/)
})
it('fails to stat non-existent file', async () => {
await expect(ipfs.files.stat('/i-do-not-exist')).to.eventually.be.rejectedWith(/does not exist/)
})
it('stats an empty directory', async () => {
const path = `/directory-${Math.random()}`
await ipfs.files.mkdir(path)
await expect(ipfs.files.stat(path)).to.eventually.include({
size: 0,
cumulativeSize: 4,
blocks: 0,
type: 'directory'
})
})
it.skip('computes how much of the DAG is local', async () => {
})
it('stats a small file', async () => {
const filePath = `/stat-${Math.random()}/small-file-${Math.random()}.txt`
await ipfs.files.write(filePath, smallFile, {
create: true,
parents: true
})
await expect(ipfs.files.stat(filePath)).to.eventually.include({
size: smallFile.length,
cumulativeSize: 71,
blocks: 1,
type: 'file'
})
})
it('stats a large file', async () => {
const filePath = `/stat-${Math.random()}/large-file-${Math.random()}.txt`
await ipfs.files.write(filePath, largeFile, {
create: true,
parents: true
})
await expect(ipfs.files.stat(filePath)).to.eventually.include({
size: largeFile.length,
cumulativeSize: 490800,
blocks: 2,
type: 'file'
})
})
it('should stat a large browser File', async function () {
if (!isBrowser) {
this.skip()
}
const filePath = `/stat-${Math.random()}/large-file-${Math.random()}.txt`
const blob = new Blob([largeFile])
await ipfs.files.write(filePath, blob, {
create: true,
parents: true
})
await expect(ipfs.files.stat(filePath)).to.eventually.include({
size: largeFile.length,
cumulativeSize: 490800,
blocks: 2,
type: 'file'
})
})
it('stats a raw node', async () => {
const filePath = `/stat-${Math.random()}/large-file-${Math.random()}.txt`
await ipfs.files.write(filePath, largeFile, {
create: true,
parents: true,
rawLeaves: true
})
const stats = await ipfs.files.stat(filePath)
const { value: node } = await ipfs.dag.get(stats.cid)
expect(node).to.have.nested.property('Links[0].Hash.code', raw.code)
const child = node.Links[0]
const rawNodeStats = await ipfs.files.stat(`/ipfs/${child.Hash}`)
expect(rawNodeStats.cid.toString()).to.equal(child.Hash.toString())
expect(rawNodeStats.type).to.equal('file') // this is what go does
})
it('stats a raw node in an mfs directory', async () => {
const filePath = `/stat-${Math.random()}/large-file-${Math.random()}.txt`
await ipfs.files.write(filePath, largeFile, {
create: true,
parents: true,
rawLeaves: true
})
const stats = await ipfs.files.stat(filePath)
const { value: node } = await ipfs.dag.get(stats.cid)
const child = node.Links[0]
expect(child.Hash.code).to.equal(raw.code)
const dir = `/dir-with-raw-${Math.random()}`
const path = `${dir}/raw-${Math.random()}`
await ipfs.files.mkdir(dir)
await ipfs.files.cp(`/ipfs/${child.Hash}`, path)
const rawNodeStats = await ipfs.files.stat(path)
expect(rawNodeStats.cid.toString()).to.equal(child.Hash.toString())
expect(rawNodeStats.type).to.equal('file') // this is what go does
})
it('stats a dag-cbor node', async () => {
const path = '/cbor.node'
const node = {}
const cid = await ipfs.dag.put(node, {
storeCodec: 'dag-cbor',
hashAlg: 'sha2-256'
})
await ipfs.files.cp(`/ipfs/${cid}`, path)
const stats = await ipfs.files.stat(path)
expect(stats.cid.toString()).to.equal(cid.toString())
})
it('stats an identity CID', async () => {
const data = uint8ArrayFromString('derp')
const path = `/test-${nanoid()}/identity.node`
const hash = await identity.digest(data)
const cid = CID.createV1(identity.code, hash)
await ipfs.block.put(data, {
mhtype: 'identity'
})
await ipfs.files.cp(`/ipfs/${cid}`, path, {
parents: true
})
const stats = await ipfs.files.stat(path)
expect(stats.cid.toString()).to.equal(cid.toString())
expect(stats).to.have.property('size', data.length)
})
it('should stat file with mode', async function () {
const testDir = `/test-${nanoid()}`
await ipfs.files.mkdir(testDir, { parents: true })
await ipfs.files.write(`${testDir}/b`, uint8ArrayFromString('Hello, world!'), { create: true })
const stat = await ipfs.files.stat(`${testDir}/b`)
expect(stat).to.include({
mode: 0o644
})
})
it('should stat file with mtime', async function () {
const testDir = `/test-${nanoid()}`
await ipfs.files.mkdir(testDir, { parents: true })
await ipfs.files.write(`${testDir}/b`, uint8ArrayFromString('Hello, world!'), {
create: true,
mtime: {
secs: 5,
nsecs: 0
}
})
const stat = await ipfs.files.stat(`${testDir}/b`)
expect(stat).to.deep.include({
mtime: {
secs: 5,
nsecs: 0
}
})
})
it('should stat dir', async function () {
const testDir = `/test-${nanoid()}`
await ipfs.files.mkdir(testDir, { parents: true })
await ipfs.files.write(`${testDir}/a`, uint8ArrayFromString('Hello, world!'), { create: true })
const stat = await ipfs.files.stat(testDir)
expect(stat).to.include({
type: 'directory',
blocks: 1,
size: 0,
withLocality: false
})
expect(stat.local).to.be.undefined()
expect(stat.sizeLocal).to.be.undefined()
})
it('should stat dir with mode', async function () {
const testDir = `/test-${nanoid()}`
await ipfs.files.mkdir(testDir, { parents: true })
const stat = await ipfs.files.stat(testDir)
expect(stat).to.include({
mode: 0o755
})
})
it('should stat dir with mtime', async function () {
const testDir = `/test-${nanoid()}`
await ipfs.files.mkdir(testDir, {
parents: true,
mtime: {
secs: 5,
nsecs: 0
}
})
const stat = await ipfs.files.stat(testDir)
expect(stat).to.deep.include({
mtime: {
secs: 5,
nsecs: 0
}
})
})
it('should stat sharded dir with mode', async function () {
const testDir = `/test-${nanoid()}`
await ipfs.files.mkdir(testDir, { parents: true })
await ipfs.files.write(`${testDir}/a`, uint8ArrayFromString('Hello, world!'), {
create: true,
shardSplitThreshold: 0
})
const stat = await ipfs.files.stat(testDir)
await expect(isShardAtPath(testDir, ipfs)).to.eventually.be.true()
expect(stat).to.have.property('type', 'directory')
expect(stat).to.include({
mode: 0o755
})
})
it('should stat sharded dir with mtime', async function () {
const testDir = `/test-${nanoid()}`
await ipfs.files.mkdir(testDir, {
parents: true,
mtime: {
secs: 5,
nsecs: 0
}
})
await ipfs.files.write(`${testDir}/a`, uint8ArrayFromString('Hello, world!'), {
create: true,
shardSplitThreshold: 0
})
const stat = await ipfs.files.stat(testDir)
await expect(isShardAtPath(testDir, ipfs)).to.eventually.be.true()
expect(stat).to.have.property('type', 'directory')
expect(stat).to.deep.include({
mtime: {
secs: 5,
nsecs: 0
}
})
})
// TODO enable this test when this feature gets released on go-ipfs
it.skip('should stat withLocal file', async function () {
const stat = await ipfs.files.stat('/test/b', { withLocal: true })
expect({
...stat,
cid: stat.cid.toString()
}).to.eql({
type: 'file',
blocks: 1,
size: 13,
cid: 'QmcZojhwragQr5qhTeFAmELik623Z21e3jBTpJXoQ9si1T',
cumulativeSize: 71,
withLocality: true,
local: true,
sizeLocal: 71
})
})
// TODO enable this test when this feature gets released on go-ipfs
it.skip('should stat withLocal dir', async function () {
const stat = await ipfs.files.stat('/test', { withLocal: true })
expect({
...stat,
cid: stat.cid.toString()
}).to.eql({
type: 'directory',
blocks: 2,
size: 0,
cid: 'QmVrkkNurBCeJvPRohW5JTvJG4AxGrFg7FnmsZZUS6nJto',
cumulativeSize: 216,
withLocality: true,
local: true,
sizeLocal: 216
})
})
it('should stat outside of mfs', async () => {
const stat = await ipfs.files.stat(`/ipfs/${fixtures.smallFile.cid}`)
expect({
...stat,
cid: stat.cid.toString()
}).to.include({
type: 'file',
blocks: 0,
size: 12,
cid: fixtures.smallFile.cid.toString(),
cumulativeSize: 20,
withLocality: false
})
expect(stat.local).to.be.undefined()
expect(stat.sizeLocal).to.be.undefined()
})
describe('with sharding', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async function () {
const ipfsd = await factory.spawn({
ipfsOptions: {
EXPERIMENTAL: {
// enable sharding for js
sharding: true
},
config: {
// enable sharding for go with automatic threshold dropped to the minimum so it shards everything
Internal: {
UnixFSShardingSizeThreshold: '1B'
}
}
}
})
ipfs = ipfsd.api
})
it('stats a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const stats = await ipfs.files.stat(`${shardedDirPath}`)
expect(stats.type).to.equal('directory')
expect(stats.size).to.equal(0)
})
it('stats a file inside a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const files = []
for await (const file of ipfs.files.ls(`${shardedDirPath}`)) {
files.push(file)
}
const stats = await ipfs.files.stat(`${shardedDirPath}/${files[0].name}`)
expect(stats.type).to.equal('file')
expect(stats.size).to.equal(7)
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/files/touch.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import { nanoid } from 'nanoid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import delay from 'delay'
import all from 'it-all'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testTouch (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.files.touch', function () {
this.timeout(120 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/**
* @param {import('ipfs-unixfs').MtimeLike} mtime
* @param {import('ipfs-unixfs').MtimeLike} expectedMtime
*/
async function testMtime (mtime, expectedMtime) {
const testPath = `/test-${nanoid()}`
await ipfs.files.write(testPath, uint8ArrayFromString('Hello, world!'), {
create: true
})
const stat = await ipfs.files.stat(testPath)
expect(stat).to.not.have.deep.property('mtime', expectedMtime)
await ipfs.files.touch(testPath, {
mtime
})
const stat2 = await ipfs.files.stat(testPath)
expect(stat2).to.have.deep.nested.property('mtime', expectedMtime)
}
before(async () => { ipfs = (await factory.spawn()).api })
after(() => factory.clean())
it('should have default mtime', async function () {
this.slow(5 * 1000)
const testPath = `/test-${nanoid()}`
await ipfs.files.write(testPath, uint8ArrayFromString('Hello, world!'), {
create: true
})
const stat = await ipfs.files.stat(testPath)
expect(stat).to.not.have.property('mtime')
await ipfs.files.touch(testPath)
const stat2 = await ipfs.files.stat(testPath)
expect(stat2).to.have.property('mtime').that.does.not.deep.equal({
secs: 0,
nsecs: 0
})
})
it('should update file mtime', async function () {
this.slow(5 * 1000)
const testPath = `/test-${nanoid()}`
const mtime = new Date()
const seconds = Math.floor(mtime.getTime() / 1000)
await ipfs.files.write(testPath, uint8ArrayFromString('Hello, world!'), {
create: true,
mtime
})
await delay(2000)
await ipfs.files.touch(testPath)
const stat = await ipfs.files.stat(testPath)
expect(stat).to.have.nested.property('mtime.secs').that.is.greaterThan(seconds)
})
it('should update directory mtime', async function () {
this.slow(5 * 1000)
const testPath = `/test-${nanoid()}`
const mtime = new Date()
const seconds = Math.floor(mtime.getTime() / 1000)
await ipfs.files.mkdir(testPath, {
create: true,
mtime
})
await delay(2000)
await ipfs.files.touch(testPath)
const stat2 = await ipfs.files.stat(testPath)
expect(stat2).to.have.nested.property('mtime.secs').that.is.greaterThan(seconds)
})
it('should update the mtime for a hamt-sharded-directory', async () => {
const path = `/foo-${Math.random()}`
await ipfs.files.mkdir(path, {
mtime: new Date()
})
await ipfs.files.write(`${path}/foo.txt`, uint8ArrayFromString('Hello world'), {
create: true,
shardSplitThreshold: 0
})
const originalMtime = (await ipfs.files.stat(path)).mtime
if (!originalMtime) {
throw new Error('No originalMtime found')
}
await delay(1000)
await ipfs.files.touch(path, {
flush: true
})
const updatedMtime = (await ipfs.files.stat(path)).mtime
if (!updatedMtime) {
throw new Error('No updatedMtime found')
}
expect(updatedMtime.secs).to.be.greaterThan(originalMtime.secs)
})
it('should create an empty file', async () => {
const path = `/foo-${Math.random()}`
await ipfs.files.touch(path, {
flush: true
})
const bytes = uint8ArrayConcat(await all(ipfs.files.read(path)))
expect(bytes.slice()).to.deep.equal(Uint8Array.from([]))
})
it('should set mtime as Date', async function () {
await testMtime(new Date(5000), {
secs: 5,
nsecs: 0
})
})
it('should set mtime as { nsecs, secs }', async function () {
const mtime = {
secs: 5,
nsecs: 0
}
await testMtime(mtime, mtime)
})
it('should set mtime as timespec', async function () {
await testMtime({
Seconds: 5,
FractionalNanoseconds: 0
}, {
secs: 5,
nsecs: 0
})
})
it('should set mtime as hrtime', async function () {
const mtime = process.hrtime()
await testMtime(mtime, {
secs: mtime[0],
nsecs: mtime[1]
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/files/write.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import { nanoid } from 'nanoid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { isNode } from 'ipfs-utils/src/env.js'
import { sha512 } from 'multiformats/hashes/sha2'
import { traverseLeafNodes } from '../utils/traverse-leaf-nodes.js'
import { createShardedDirectory } from '../utils/create-sharded-directory.js'
import { createTwoShards } from '../utils/create-two-shards.js'
import { randomBytes, randomStream } from 'iso-random-stream'
import all from 'it-all'
import isShardAtPath from '../utils/is-shard-at-path.js'
import * as raw from 'multiformats/codecs/raw'
import map from 'it-map'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testWrite (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
const smallFile = randomBytes(13)
const largeFile = randomBytes(490668)
/**
* @param {(arg: { type: string, path: string, content: Uint8Array | AsyncIterable, contentSize: number }) => void} fn
*/
const runTest = (fn) => {
const iterations = 5
const files = [{
type: 'Small file',
path: `/small-file-${Math.random()}.txt`,
content: smallFile,
contentSize: smallFile.length
}, {
type: 'Large file',
path: `/large-file-${Math.random()}.jpg`,
content: largeFile,
contentSize: largeFile.length
}, {
type: 'Really large file',
path: `/really-large-file-${Math.random()}.jpg`,
content: {
[Symbol.asyncIterator]: function * () {
for (let i = 0; i < iterations; i++) {
yield largeFile
}
}
},
contentSize: largeFile.length * iterations
}]
files.forEach((file) => {
fn(file)
})
}
describe('.files.write', function () {
this.timeout(300 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/**
* @param {number | string} mode
* @param {number} expectedMode
*/
async function testMode (mode, expectedMode) {
const testPath = `/test-${nanoid()}`
await ipfs.files.write(testPath, uint8ArrayFromString('Hello, world!'), {
create: true,
parents: true,
mode
})
const stats = await ipfs.files.stat(testPath)
expect(stats).to.have.property('mode', expectedMode)
}
/**
* @param {import('ipfs-unixfs').MtimeLike} mtime
* @param {import('ipfs-unixfs').MtimeLike} expectedMtime
*/
async function testMtime (mtime, expectedMtime) {
const testPath = `/test-${nanoid()}`
await ipfs.files.write(testPath, uint8ArrayFromString('Hello, world!'), {
create: true,
parents: true,
mtime
})
const stats = await ipfs.files.stat(testPath)
expect(stats).to.have.deep.property('mtime', expectedMtime)
}
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('explodes if it cannot convert content to a source', async () => {
// @ts-expect-error invalid arg
await expect(ipfs.files.write('/foo-bad-source', -1, {
create: true
})).to.eventually.be.rejected()
})
it('explodes if given an invalid path', async () => {
// @ts-expect-error invalid arg
await expect(ipfs.files.write('foo-no-slash', null, {
create: true
})).to.eventually.be.rejected()
})
it('explodes if given a negative offset', async () => {
await expect(ipfs.files.write('/foo-negative-offset', uint8ArrayFromString('foo'), {
offset: -1
})).to.eventually.be.rejected()
})
it('explodes if given a negative length', async () => {
await expect(ipfs.files.write('/foo-negative-length', uint8ArrayFromString('foo'), {
length: -1
})).to.eventually.be.rejected()
})
it('creates a zero length file when passed a zero length', async () => {
const path = '/foo-zero-length'
await ipfs.files.write(path, uint8ArrayFromString('foo'), {
length: 0,
create: true
})
await expect(all(ipfs.files.ls(path))).to.eventually.have.lengthOf(1)
.and.to.have.nested.property('[0]').that.include({
name: 'foo-zero-length',
size: 0
})
})
it('writes a small file using a buffer', async () => {
const filePath = `/small-file-${Math.random()}.txt`
await ipfs.files.write(filePath, smallFile, {
create: true
})
await expect(ipfs.files.stat(filePath)).to.eventually.have.property('size', smallFile.length)
expect(uint8ArrayConcat(await all(ipfs.files.read(filePath)))).to.deep.equal(smallFile)
})
it('writes a small file using a string', async function () {
const filePath = `/string-${Math.random()}.txt`
const content = 'hello world'
await ipfs.files.write(filePath, content, {
create: true
})
await expect(ipfs.files.stat(filePath)).to.eventually.have.property('size', content.length)
expect(uint8ArrayConcat(await all(ipfs.files.read(filePath)))).to.deep.equal(uint8ArrayFromString(content))
})
it('writes part of a small file using a string', async function () {
const filePath = `/string-${Math.random()}.txt`
const content = 'hello world'
await ipfs.files.write(filePath, content, {
create: true,
length: 2
})
const stats = await ipfs.files.stat(filePath)
expect(stats.size).to.equal(2)
})
it('writes a small file using a Node stream (Node only)', async function () {
if (!isNode) {
this.skip()
}
const filePath = `/small-file-${Math.random()}.txt`
const stream = randomStream(1000)
await ipfs.files.write(filePath, stream, {
create: true
})
const stats = await ipfs.files.stat(filePath)
expect(stats.size).to.equal(1000)
})
it('writes a small file using an HTML5 Blob (Browser only)', async function () {
if (!global.Blob || !global.FileReader) {
return this.skip()
}
const filePath = `/small-file-${Math.random()}.txt`
const blob = new global.Blob([smallFile.buffer.slice(smallFile.byteOffset, smallFile.byteOffset + smallFile.byteLength)])
await ipfs.files.write(filePath, blob, {
create: true
})
const stats = await ipfs.files.stat(filePath)
expect(stats.size).to.equal(smallFile.length)
})
it('writes a small file with an escaped slash in the title', async () => {
const filePath = `/small-\\/file-${Math.random()}.txt`
await ipfs.files.write(filePath, smallFile, {
create: true
})
const stats = await ipfs.files.stat(filePath)
expect(stats.size).to.equal(smallFile.length)
await expect(ipfs.files.stat('/small-\\')).to.eventually.rejectedWith(/does not exist/)
})
it('writes a deeply nested small file', async () => {
const filePath = '/foo/bar/baz/qux/quux/garply/small-file.txt'
await ipfs.files.write(filePath, smallFile, {
create: true,
parents: true
})
const stats = await ipfs.files.stat(filePath)
expect(stats.size).to.equal(smallFile.length)
})
it('refuses to write to a file in a folder that does not exist', async () => {
const filePath = `/${Math.random()}/small-file.txt`
try {
await ipfs.files.write(filePath, smallFile, {
create: true
})
throw new Error('Writing a file to a non-existent folder without the --parents flag should have failed')
} catch (/** @type {any} */ err) {
expect(err.message).to.contain('does not exist')
}
})
it('refuses to write to a file that does not exist', async () => {
const filePath = `/small-file-${Math.random()}.txt`
try {
await ipfs.files.write(filePath, smallFile)
throw new Error('Writing a file to a non-existent file without the --create flag should have failed')
} catch (/** @type {any} */ err) {
expect(err.message).to.contain('file does not exist')
}
})
it('refuses to write to a path that has a file in it', async () => {
const filePath = `/small-file-${Math.random()}.txt`
await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3]), {
create: true
})
try {
await ipfs.files.write(`${filePath}/other-file-${Math.random()}.txt`, Uint8Array.from([0, 1, 2, 3]), {
create: true
})
throw new Error('Writing a path with a file in it should have failed')
} catch (/** @type {any} */ err) {
expect(err.message).to.contain('Not a directory')
}
})
runTest(({ type, path, content }) => {
it(`limits how many bytes to write to a file (${type})`, async () => {
await ipfs.files.write(path, content, {
create: true,
parents: true,
length: 2
})
const bytes = uint8ArrayConcat(await all(ipfs.files.read(path)))
expect(bytes.length).to.equal(2)
})
})
runTest(({ type, path, content, contentSize }) => {
it(`overwrites start of a file without truncating (${type})`, async () => {
const newContent = uint8ArrayFromString('Goodbye world')
await ipfs.files.write(path, content, {
create: true
})
await expect(ipfs.files.stat(path)).to.eventually.have.property('size', contentSize)
await ipfs.files.write(path, newContent)
const stats = await ipfs.files.stat(path)
expect(stats.size).to.equal(contentSize)
const buffer = uint8ArrayConcat(await all(ipfs.files.read(path, {
offset: 0,
length: newContent.length
})))
expect(buffer).to.deep.equal(newContent)
})
})
runTest(({ type, path, content, contentSize }) => {
it(`pads the start of a new file when an offset is specified (${type})`, async () => {
const offset = 10
await ipfs.files.write(path, content, {
offset,
create: true
})
await expect(ipfs.files.stat(path)).to.eventually.have.property('size', offset + contentSize)
const buffer = uint8ArrayConcat(await all(ipfs.files.read(path, {
offset: 0,
length: offset
})))
expect(buffer).to.deep.equal(new Uint8Array(offset))
})
})
runTest(({ type, path, content, contentSize }) => {
it(`expands a file when an offset is specified (${type})`, async () => {
const offset = contentSize - 1
const newContent = uint8ArrayFromString('Oh hai!')
await ipfs.files.write(path, content, {
create: true
})
await ipfs.files.write(path, newContent, {
offset
})
await expect(ipfs.files.stat(path)).to.eventually.have.property('size', contentSize + newContent.length - 1)
const buffer = uint8ArrayConcat(await all(ipfs.files.read(path, {
offset: offset
})))
expect(buffer).to.deep.equal(newContent)
})
})
runTest(({ type, path, content, contentSize }) => {
it(`expands a file when an offset is specified and the offset is longer than the file (${type})`, async () => {
const offset = contentSize + 5
const newContent = uint8ArrayFromString('Oh hai!')
await ipfs.files.write(path, content, {
create: true
})
await ipfs.files.write(path, newContent, {
offset
})
await expect(ipfs.files.stat(path)).to.eventually.have.property('size', newContent.length + offset)
const buffer = uint8ArrayConcat(await all(ipfs.files.read(path)))
if (!(content instanceof Uint8Array)) {
content = uint8ArrayConcat(await all(content))
}
expect(buffer).to.deep.equal(uint8ArrayConcat([content, Uint8Array.from([0, 0, 0, 0, 0]), newContent]))
})
})
runTest(({ type, path, content }) => {
it(`truncates a file after writing (${type})`, async () => {
const newContent = uint8ArrayFromString('Oh hai!')
await ipfs.files.write(path, content, {
create: true
})
await ipfs.files.write(path, newContent, {
truncate: true
})
await expect(ipfs.files.stat(path)).to.eventually.have.property('size', newContent.length)
const buffer = uint8ArrayConcat(await all(ipfs.files.read(path)))
expect(buffer).to.deep.equal(newContent)
})
})
runTest(({ type, path, content }) => {
it(`writes a file with raw blocks for newly created leaf nodes (${type})`, async () => {
await ipfs.files.write(path, content, {
create: true,
rawLeaves: true
})
const stats = await ipfs.files.stat(path)
let leafCount = 0
for await (const { cid } of traverseLeafNodes(ipfs, stats.cid)) {
leafCount++
expect(cid.code).to.equal(raw.code)
}
expect(leafCount).to.be.greaterThan(0)
})
})
it('supports concurrent writes', async function () {
/** @type {{ name: string, source: ReturnType}[]} */
const files = []
for (let i = 0; i < 10; i++) {
files.push({
name: `source-file-${Math.random()}.txt`,
source: randomBytes(100)
})
}
await Promise.all(
files.map(({ name, source }) => ipfs.files.write(`/concurrent/${name}`, source, {
create: true,
parents: true
}))
)
const listing = await all(ipfs.files.ls('/concurrent'))
expect(listing.length).to.equal(files.length)
listing.forEach(listedFile => {
expect(files.find(file => file.name === listedFile.name))
})
})
it('rewrites really big files', async function () {
const initialStream = randomBytes(1024 * 300)
const newDataStream = randomBytes(1024 * 300)
const fileName = `/rewrite/file-${Math.random()}.txt`
await ipfs.files.write(fileName, initialStream, {
create: true,
parents: true
})
await ipfs.files.write(fileName, newDataStream, {
offset: 0
})
const actualBytes = uint8ArrayConcat(await all(ipfs.files.read(fileName)))
for (let i = 0; i < newDataStream.length; i++) {
if (newDataStream[i] !== actualBytes[i]) {
if (initialStream[i] === actualBytes[i]) {
throw new Error(`Bytes at index ${i} were not overwritten - expected ${newDataStream[i]} actual ${initialStream[i]}`)
}
throw new Error(`Bytes at index ${i} not equal - expected ${newDataStream[i]} actual ${actualBytes[i]}`)
}
}
expect(actualBytes).to.deep.equal(newDataStream)
})
it('writes a file with a different CID version to the parent', async () => {
const directory = `cid-versions-${Math.random()}`
const directoryPath = `/${directory}`
const fileName = `file-${Math.random()}.txt`
const filePath = `${directoryPath}/${fileName}`
const expectedBytes = Uint8Array.from([0, 1, 2, 3])
await ipfs.files.mkdir(directoryPath, {
cidVersion: 0
})
await expect(ipfs.files.stat(directoryPath)).to.eventually.have.nested.property('cid.version', 0)
await ipfs.files.write(filePath, expectedBytes, {
create: true,
cidVersion: 1
})
await expect(ipfs.files.stat(filePath)).to.eventually.have.nested.property('cid.version', 1)
const actualBytes = uint8ArrayConcat(await all(ipfs.files.read(filePath)))
expect(actualBytes).to.deep.equal(expectedBytes)
})
it('overwrites a file with a different CID version', async () => {
const directory = `cid-versions-${Math.random()}`
const directoryPath = `/${directory}`
const fileName = `file-${Math.random()}.txt`
const filePath = `${directoryPath}/${fileName}`
const expectedBytes = Uint8Array.from([0, 1, 2, 3])
await ipfs.files.mkdir(directoryPath, {
cidVersion: 0
})
await expect(ipfs.files.stat(directoryPath)).to.eventually.have.nested.property('cid.version', 0)
await ipfs.files.write(filePath, Uint8Array.from([5, 6]), {
create: true,
cidVersion: 0
})
await expect(ipfs.files.stat(filePath)).to.eventually.have.nested.property('cid.version', 0)
await ipfs.files.write(filePath, expectedBytes, {
cidVersion: 1
})
await expect(ipfs.files.stat(filePath)).to.eventually.have.nested.property('cid.version', 1)
const actualBytes = uint8ArrayConcat(await all(ipfs.files.read(filePath)))
expect(actualBytes).to.deep.equal(expectedBytes)
})
it('partially overwrites a file with a different CID version', async () => {
const directory = `cid-versions-${Math.random()}`
const directoryPath = `/${directory}`
const fileName = `file-${Math.random()}.txt`
const filePath = `${directoryPath}/${fileName}`
await ipfs.files.mkdir(directoryPath, {
cidVersion: 0
})
await expect(ipfs.files.stat(directoryPath)).to.eventually.have.nested.property('cid.version', 0)
await ipfs.files.write(filePath, Uint8Array.from([5, 6, 7, 8, 9, 10, 11]), {
create: true,
cidVersion: 0
})
await expect(ipfs.files.stat(filePath)).to.eventually.have.nested.property('cid.version', 0)
await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3]), {
cidVersion: 1,
offset: 1
})
await expect(ipfs.files.stat(filePath)).to.eventually.have.nested.property('cid.version', 1)
const actualBytes = uint8ArrayConcat(await all(ipfs.files.read(filePath)))
expect(actualBytes).to.deep.equal(Uint8Array.from([5, 0, 1, 2, 3, 10, 11]))
})
it('writes a file with a different hash function to the parent', async () => {
const directory = `cid-versions-${Math.random()}`
const directoryPath = `/${directory}`
const fileName = `file-${Math.random()}.txt`
const filePath = `${directoryPath}/${fileName}`
const expectedBytes = Uint8Array.from([0, 1, 2, 3])
await ipfs.files.mkdir(directoryPath, {
cidVersion: 0
})
await expect(ipfs.files.stat(directoryPath)).to.eventually.have.nested.property('cid.version', 0)
await ipfs.files.write(filePath, expectedBytes, {
create: true,
cidVersion: 1,
hashAlg: 'sha2-512'
})
await expect(ipfs.files.stat(filePath)).to.eventually.have.nested.property('cid.multihash.code', sha512.code)
const actualBytes = uint8ArrayConcat(await all(ipfs.files.read(filePath)))
expect(actualBytes).to.deep.equal(expectedBytes)
})
it('should write file and specify mode as a string', async function () {
const mode = '0321'
await testMode(mode, parseInt(mode, 8))
})
it('should write file and specify mode as a number', async function () {
const mode = parseInt('0321', 8)
await testMode(mode, mode)
})
it('should write file and specify mtime as Date', async function () {
const mtime = new Date()
const seconds = Math.floor(mtime.getTime() / 1000)
const expectedMtime = {
secs: seconds,
nsecs: (mtime.getTime() - (seconds * 1000)) * 1000
}
await testMtime(mtime, expectedMtime)
})
it('should write file and specify mtime as { nsecs, secs }', async function () {
const mtime = {
secs: 5,
nsecs: 0
}
await testMtime(mtime, mtime)
})
it('should write file and specify mtime as timespec', async function () {
await testMtime({
Seconds: 5,
FractionalNanoseconds: 0
}, {
secs: 5,
nsecs: 0
})
})
it('should write file and specify mtime as hrtime', async function () {
const mtime = process.hrtime()
await testMtime(mtime, {
secs: mtime[0],
nsecs: mtime[1]
})
})
describe('with sharding', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async function () {
const ipfsd = await factory.spawn({
ipfsOptions: {
EXPERIMENTAL: {
// enable sharding for js
sharding: true
},
config: {
// enable sharding for go with automatic threshold dropped to the minimum so it shards everything
Internal: {
UnixFSShardingSizeThreshold: '1B'
}
}
}
})
ipfs = ipfsd.api
})
it('shards a large directory when writing too many links to it', async () => {
const shardSplitThreshold = 10
const dirPath = `/sharded-dir-${Math.random()}`
const newFile = `file-${Math.random()}`
const newFilePath = `/${dirPath}/${newFile}`
await ipfs.files.mkdir(dirPath, {
shardSplitThreshold
})
for (let i = 0; i < shardSplitThreshold; i++) {
await ipfs.files.write(`/${dirPath}/file-${Math.random()}`, Uint8Array.from([0, 1, 2, 3]), {
create: true,
shardSplitThreshold
})
}
await expect(ipfs.files.stat(dirPath)).to.eventually.have.property('type', 'directory')
await ipfs.files.write(newFilePath, Uint8Array.from([0, 1, 2, 3]), {
create: true,
shardSplitThreshold
})
await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true()
await expect(ipfs.files.stat(dirPath)).to.eventually.have.property('type', 'directory')
const files = await all(ipfs.files.ls(dirPath, {
long: true
}))
// new file should be in directory
expect(files.filter(file => file.name === newFile).pop()).to.be.ok()
})
it('results in the same hash as a sharded directory created by the importer when adding a new file', async function () {
const {
nextFile,
dirWithSomeFiles,
dirPath
} = await createTwoShards(ipfs, 75)
await ipfs.files.cp(`/ipfs/${dirWithSomeFiles}`, dirPath)
await ipfs.files.write(nextFile.path, nextFile.content, {
create: true
})
const stats = await ipfs.files.stat(dirPath)
const updatedDirCid = stats.cid
await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true()
expect(stats.type).to.equal('directory')
expect(updatedDirCid.toString()).to.equal('QmbLw9uCrQaFgweMskqMrsVKTwwakSg94GuMT3zht1P7CQ')
})
it('results in the same hash as a sharded directory created by the importer when creating a new subshard', async function () {
const {
nextFile,
dirWithSomeFiles,
dirPath
} = await createTwoShards(ipfs, 100)
await ipfs.files.cp(`/ipfs/${dirWithSomeFiles}`, dirPath)
await ipfs.files.write(nextFile.path, nextFile.content, {
create: true
})
const stats = await ipfs.files.stat(dirPath)
const updatedDirCid = stats.cid
expect(updatedDirCid.toString()).to.equal('QmcGTKoaZeMxVenyxnkP2riibE8vSEPobkN1oxvcEZpBW5')
})
it('results in the same hash as a sharded directory created by the importer when adding a file to a subshard', async function () {
const {
nextFile,
dirWithSomeFiles,
dirPath
} = await createTwoShards(ipfs, 82)
await ipfs.files.cp(`/ipfs/${dirWithSomeFiles}`, dirPath)
await ipfs.files.write(nextFile.path, nextFile.content, {
create: true
})
const stats = await ipfs.files.stat(dirPath)
const updatedDirCid = stats.cid
await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true()
expect(stats.type).to.equal('directory')
expect(updatedDirCid.toString()).to.deep.equal('QmXeJ4ercHcxdiX7Vxm1Hit9AwsTNXcwCw5Ad32yW2HdHR')
})
it('results in the same hash as a sharded directory created by the importer when adding a file to a subshard of a subshard', async function () {
const {
nextFile,
dirWithSomeFiles,
dirPath
} = await createTwoShards(ipfs, 2187)
await ipfs.files.cp(`/ipfs/${dirWithSomeFiles}`, dirPath)
await ipfs.files.write(nextFile.path, nextFile.content, {
create: true
})
const stats = await ipfs.files.stat(dirPath)
const updatedDirCid = stats.cid
await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true()
expect(stats.type).to.equal('directory')
expect(updatedDirCid.toString()).to.deep.equal('QmY4o7GNvr5eZPnT6k6ALp5zkQ4eiUkJQ6eeUNsdSiqS4f')
})
it('writes a file to an already sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const newFile = `file-${Math.random()}`
const newFilePath = `${shardedDirPath}/${newFile}`
await ipfs.files.write(newFilePath, Uint8Array.from([0, 1, 2, 3]), {
create: true
})
// should still be a sharded directory
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
await expect(ipfs.files.stat(shardedDirPath)).to.eventually.have.property('type', 'directory')
const files = await all(ipfs.files.ls(shardedDirPath, {
long: true
}))
// new file should be in the directory
expect(files.filter(file => file.name === newFile).pop()).to.be.ok()
// should be able to ls new file directly
await expect(all(ipfs.files.ls(newFilePath, {
long: true
}))).to.eventually.not.be.empty()
})
it('overwrites a file in a sharded directory when positions do not match', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const newFile = 'file-0.6944395883502592'
const newFilePath = `${shardedDirPath}/${newFile}`
const newContent = Uint8Array.from([3, 2, 1, 0])
await ipfs.files.write(newFilePath, Uint8Array.from([0, 1, 2, 3]), {
create: true
})
// should still be a sharded directory
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
await expect(ipfs.files.stat(shardedDirPath)).to.eventually.have.property('type', 'directory')
// overwrite the file
await ipfs.files.write(newFilePath, newContent, {
create: true
})
// read the file back
const buffer = uint8ArrayConcat(await all(ipfs.files.read(newFilePath)))
expect(buffer).to.deep.equal(newContent)
// should be able to ls new file directly
await expect(all(ipfs.files.ls(newFilePath, {
long: true
}))).to.eventually.not.be.empty()
})
it('overwrites file in a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const newFile = `file-${Math.random()}`
const newFilePath = `${shardedDirPath}/${newFile}`
const newContent = Uint8Array.from([3, 2, 1, 0])
await ipfs.files.write(newFilePath, Uint8Array.from([0, 1, 2, 3]), {
create: true
})
// should still be a sharded directory
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
await expect(ipfs.files.stat(shardedDirPath)).to.eventually.have.property('type', 'directory')
// overwrite the file
await ipfs.files.write(newFilePath, newContent, {
create: true
})
// read the file back
const buffer = uint8ArrayConcat(await all(ipfs.files.read(newFilePath)))
expect(buffer).to.deep.equal(newContent)
// should be able to ls new file directly
await expect(all(ipfs.files.ls(newFilePath, {
long: true
}))).to.eventually.not.be.empty()
})
it('overwrites a file in a subshard of a sharded directory', async () => {
const shardedDirPath = await createShardedDirectory(ipfs)
const newFile = 'file-1a.txt'
const newFilePath = `${shardedDirPath}/${newFile}`
const newContent = Uint8Array.from([3, 2, 1, 0])
await ipfs.files.write(newFilePath, Uint8Array.from([0, 1, 2, 3]), {
create: true
})
// should still be a sharded directory
await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true()
await expect(ipfs.files.stat(shardedDirPath)).to.eventually.have.property('type', 'directory')
// overwrite the file
await ipfs.files.write(newFilePath, newContent, {
create: true
})
// read the file back
const buffer = uint8ArrayConcat(await all(ipfs.files.read(newFilePath)))
expect(buffer).to.deep.equal(newContent)
// should be able to ls new file directly
await expect(all(ipfs.files.ls(newFilePath, {
long: true
}))).to.eventually.not.be.empty()
})
it('writes a file to a sub-shard of a shard that contains another sub-shard', async () => {
const data = Uint8Array.from([0, 1, 2])
await ipfs.files.mkdir('/hamttest-mfs')
const files = [
'file-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-1398.txt',
'vivanov-sliceart',
'file-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-1230.txt',
'methodify',
'fis-msprd-style-loader_0_13_1',
'js-form',
'file-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-1181.txt',
'node-gr',
'yanvoidmodule',
'file-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-1899.txt',
'file-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-372.txt',
'file-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-1032.txt',
'file-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-1293.txt',
'file-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-766.txt'
]
for (const path of files) {
await ipfs.files.write(`/hamttest-mfs/${path}`, data, {
shardSplitThreshold: 0,
create: true
})
}
const beforeFiles = await all(map(ipfs.files.ls('/hamttest-mfs'), (entry) => entry.name))
expect(beforeFiles).to.have.lengthOf(files.length)
await ipfs.files.write('/hamttest-mfs/supermodule_test', data, {
shardSplitThreshold: 0,
create: true
})
const afterFiles = await all(map(ipfs.files.ls('/hamttest-mfs'), (entry) => entry.name))
expect(afterFiles).to.have.lengthOf(beforeFiles.length + 1)
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/get.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import { fixtures } from './utils/index.js'
import { CID } from 'multiformats/cid'
import all from 'it-all'
import drain from 'it-drain'
import last from 'it-last'
import map from 'it-map'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from './utils/mocha.js'
import testTimeout from './utils/test-timeout.js'
import { importer } from 'ipfs-unixfs-importer'
import blockstore from './utils/blockstore-adapter.js'
import { extract } from 'it-tar'
import { pipe } from 'it-pipe'
import toBuffer from 'it-to-buffer'
import Pako from 'pako'
/**
* @typedef {import('it-stream-types').Source} Source
*/
/**
* @param {string} name
* @param {string} [path]
*/
const content = (name, path) => {
if (!path) {
path = name
}
return {
path: `test-folder/${path}`,
content: fixtures.directory.files[name]
}
}
/**
* @param {string} name
*/
const emptyDir = (name) => ({ path: `test-folder/${name}` })
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testGet (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.get', function () {
this.timeout(120 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/**
* @param {Source} source
*/
async function * gzipped (source) {
const inflator = new Pako.Inflate()
for await (const buf of source) {
inflator.push(buf, false)
}
inflator.push(new Uint8Array(0), true)
if (inflator.err) {
throw new Error(`Error ungzipping - message: "${inflator.msg}" code: ${inflator.err}`)
}
if (inflator.result instanceof Uint8Array) {
yield inflator.result
} else {
throw new Error('Unexpected gzip data type')
}
}
/**
* @param {Source} source
*/
async function * tarballed (source) {
yield * pipe(
source,
extract(),
async function * (source) {
for await (const entry of source) {
yield {
...entry,
body: await toBuffer(map(entry.body, (buf) => buf.slice()))
}
}
}
)
}
before(async () => {
ipfs = (await factory.spawn()).api
await Promise.all([
all(importer({ content: fixtures.smallFile.data }, blockstore(ipfs))),
all(importer({ content: fixtures.bigFile.data }, blockstore(ipfs)))
])
})
after(() => factory.clean())
it('should respect timeout option when getting files', () => {
return testTimeout(() => drain(ipfs.get(CID.parse('QmPDqvcuA4AkhBLBuh2y49yhUB98rCnxPxa3eVNC1kAbS1'), {
timeout: 1
})))
})
it('should get with a base58 encoded multihash', async () => {
const output = await pipe(
ipfs.get(fixtures.smallFile.cid),
tarballed,
(source) => all(source)
)
expect(output).to.have.lengthOf(1)
expect(output).to.have.nested.property('[0].header.name', fixtures.smallFile.cid.toString())
expect(output).to.have.nested.property('[0].body').that.equalBytes(fixtures.smallFile.data)
})
it('should get a file added as CIDv0 with a CIDv1', async () => {
const input = uint8ArrayFromString(`TEST${Math.random()}`)
const res = await all(importer({ content: input }, blockstore(ipfs)))
const cidv0 = res[0].cid
expect(cidv0.version).to.equal(0)
const cidv1 = cidv0.toV1()
const output = await pipe(
ipfs.get(cidv1),
tarballed,
(source) => all(source)
)
expect(output).to.have.lengthOf(1)
expect(output).to.have.nested.property('[0].header.name', cidv1.toString())
expect(output).to.have.nested.property('[0].body').that.equalBytes(input)
})
it('should get a file added as CIDv1 with a CIDv0', async () => {
const input = uint8ArrayFromString(`TEST${Math.random()}`)
const res = await all(importer({ content: input }, blockstore(ipfs), { cidVersion: 1, rawLeaves: false }))
const cidv1 = res[0].cid
expect(cidv1.version).to.equal(1)
const cidv0 = cidv1.toV0()
const output = await pipe(
ipfs.get(cidv0),
tarballed,
(source) => all(source)
)
expect(output).to.have.lengthOf(1)
expect(output).to.have.nested.property('[0].header.name', cidv0.toString())
expect(output).to.have.nested.property('[0].body').that.equalBytes(input)
})
it('should get a file added as CIDv1 with rawLeaves', async () => {
const input = uint8ArrayFromString(`TEST${Math.random()}`)
const res = await all(importer({ content: input }, blockstore(ipfs), { cidVersion: 1, rawLeaves: true }))
const cidv1 = res[0].cid
expect(cidv1.version).to.equal(1)
const output = await pipe(
ipfs.get(cidv1),
tarballed,
(source) => all(source)
)
expect(output).to.have.lengthOf(1)
expect(output).to.have.nested.property('[0].header.name', cidv1.toString())
expect(output).to.have.nested.property('[0].body').that.equalBytes(input)
})
it('should get a BIG file', async () => {
const output = await pipe(
ipfs.get(fixtures.bigFile.cid),
tarballed,
(source) => all(source)
)
expect(output).to.have.lengthOf(1)
expect(output).to.have.nested.property('[0].header.name', fixtures.bigFile.cid.toString())
expect(output).to.have.nested.property('[0].body').that.equalBytes(fixtures.bigFile.data)
})
it('should get a directory', async function () {
const dirs = [
content('pp.txt'),
content('holmes.txt'),
content('jungle.txt'),
content('alice.txt'),
emptyDir('empty-folder'),
content('files/hello.txt'),
content('files/ipfs.txt'),
emptyDir('files/empty')
]
const res = await all(importer(dirs, blockstore(ipfs)))
const { cid } = res[res.length - 1]
expect(`${cid}`).to.equal(fixtures.directory.cid.toString())
const output = await pipe(
ipfs.get(cid),
tarballed,
(source) => all(source)
)
// Check paths
const paths = output.map((file) => { return file.header.name })
expect(paths).to.include.members([
'QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP',
'QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/alice.txt',
'QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/empty-folder',
'QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/files',
'QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/files/empty',
'QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/files/hello.txt',
'QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/files/ipfs.txt',
'QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/holmes.txt',
'QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/jungle.txt',
'QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/pp.txt'
])
// Check contents
expect(output.map(f => uint8ArrayToString(f.body))).to.include.members([
uint8ArrayToString(fixtures.directory.files['alice.txt']),
uint8ArrayToString(fixtures.directory.files['files/hello.txt']),
uint8ArrayToString(fixtures.directory.files['files/ipfs.txt']),
uint8ArrayToString(fixtures.directory.files['holmes.txt']),
uint8ArrayToString(fixtures.directory.files['jungle.txt']),
uint8ArrayToString(fixtures.directory.files['pp.txt'])
])
})
it('should get a nested directory', async function () {
const dirs = [
content('pp.txt', 'pp.txt'),
content('holmes.txt', 'foo/holmes.txt'),
content('jungle.txt', 'foo/bar/jungle.txt')
]
const res = await all(importer(dirs, blockstore(ipfs)))
const { cid } = res[res.length - 1]
expect(`${cid}`).to.equal('QmVMXXo3c2bDPH9ayy2VKoXpykfYJHwAcU5YCJjPf7jg3g')
const output = await pipe(
ipfs.get(cid),
tarballed,
(source) => all(source)
)
// Check paths
expect(output.map((file) => { return file.header.name })).to.include.members([
'QmVMXXo3c2bDPH9ayy2VKoXpykfYJHwAcU5YCJjPf7jg3g',
'QmVMXXo3c2bDPH9ayy2VKoXpykfYJHwAcU5YCJjPf7jg3g/pp.txt',
'QmVMXXo3c2bDPH9ayy2VKoXpykfYJHwAcU5YCJjPf7jg3g/foo/holmes.txt',
'QmVMXXo3c2bDPH9ayy2VKoXpykfYJHwAcU5YCJjPf7jg3g/foo/bar/jungle.txt'
])
// Check contents
expect(output.map(f => uint8ArrayToString(f.body))).to.include.members([
uint8ArrayToString(fixtures.directory.files['pp.txt']),
uint8ArrayToString(fixtures.directory.files['holmes.txt']),
uint8ArrayToString(fixtures.directory.files['jungle.txt'])
])
})
it('should get with ipfs path, as object and nested value', async () => {
const file = {
path: 'a/testfile.txt',
content: fixtures.smallFile.data
}
const fileAdded = await last(importer([file], blockstore(ipfs)))
if (!fileAdded) {
throw new Error('No file was added')
}
expect(fileAdded).to.have.property('path', 'a')
const output = await pipe(
ipfs.get(`/ipfs/${fileAdded.cid}/testfile.txt`),
tarballed,
(source) => all(source)
)
expect(output).to.be.length(1)
expect(uint8ArrayToString(output[0].body)).to.equal('Plz add me!\n')
})
it('should compress a file directly', async () => {
const output = await pipe(
ipfs.get(fixtures.smallFile.cid, {
compress: true,
compressionLevel: 5
}),
gzipped,
(source) => all(source)
)
expect(uint8ArrayConcat(output)).to.equalBytes(fixtures.smallFile.data)
})
it('should compress a file as a tarball', async () => {
const output = await pipe(
ipfs.get(fixtures.smallFile.cid, {
archive: true,
compress: true,
compressionLevel: 5
}),
gzipped,
tarballed,
(source) => all(source)
)
expect(output).to.have.nested.property('[0].body').that.equalBytes(fixtures.smallFile.data)
})
it('should not compress a directory', async () => {
const dirs = [
content('pp.txt'),
emptyDir('empty-folder'),
content('files/hello.txt')
]
const res = await all(importer(dirs, blockstore(ipfs)))
const { cid } = res[res.length - 1]
await expect(drain(ipfs.get(cid, {
compress: true,
compressionLevel: 5
}))).to.eventually.be.rejectedWith(/file is not regular/)
})
it('should compress a file with invalid compression level', async () => {
await expect(drain(ipfs.get(fixtures.smallFile.cid, {
compress: true,
compressionLevel: 10
}))).to.eventually.be.rejected()
})
it('should compress a directory as a tarball', async () => {
const dirs = [
content('pp.txt'),
emptyDir('empty-folder'),
content('files/hello.txt')
]
const res = await all(importer(dirs, blockstore(ipfs)))
const { cid } = res[res.length - 1]
const output = await pipe(
ipfs.get(cid, {
archive: true,
compress: true,
compressionLevel: 5
}),
gzipped,
tarballed,
(source) => all(source)
)
// Check paths
const paths = output.map((file) => { return file.header.name })
expect(paths).to.include.members([
'QmXpbhYKheGs5sopefFjsABsjr363QkRaJT4miRsN88ABU',
'QmXpbhYKheGs5sopefFjsABsjr363QkRaJT4miRsN88ABU/empty-folder',
'QmXpbhYKheGs5sopefFjsABsjr363QkRaJT4miRsN88ABU/files/hello.txt',
'QmXpbhYKheGs5sopefFjsABsjr363QkRaJT4miRsN88ABU/pp.txt'
])
// Check contents
expect(output.map(f => uint8ArrayToString(f.body))).to.include.members([
uint8ArrayToString(fixtures.directory.files['files/hello.txt']),
uint8ArrayToString(fixtures.directory.files['pp.txt'])
])
})
it('should error on invalid key', async () => {
const invalidCid = 'somethingNotMultihash'
await expect(all(ipfs.get(invalidCid))).to.eventually.be.rejected()
})
it('get path containing "+"s', async () => {
const filename = 'ti,c64x+mega++mod-pic.txt'
const subdir = 'tmp/c++files'
const expectedCid = 'QmPkmARcqjo5fqK1V1o8cFsuaXxWYsnwCNLJUYS4KeZyff'
const path = `${subdir}/${filename}`
const files = await all(ipfs.addAll([{
path,
content: path
}]))
expect(files[2].cid.toString()).to.equal(expectedCid)
const cid = 'QmPkmARcqjo5fqK1V1o8cFsuaXxWYsnwCNLJUYS4KeZyff'
const output = await pipe(
ipfs.get(CID.parse(cid)),
tarballed,
(source) => all(source)
)
expect(output).to.be.an('array').with.lengthOf(3)
expect(output).to.have.nested.property('[0].header.name', cid)
expect(output).to.have.nested.property('[1].header.name', `${cid}/c++files`)
expect(output).to.have.nested.property('[2].header.name', `${cid}/c++files/ti,c64x+mega++mod-pic.txt`)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/index.js
================================================
import { createSuite } from './utils/suite.js'
import { testAdd } from './add.js'
import { testAddAll } from './add-all.js'
import { testCat } from './cat.js'
import { testGet } from './get.js'
import { testLs } from './ls.js'
import { testRefs } from './refs.js'
import { testRefsLocal } from './refs-local.js'
import testFiles from './files/index.js'
import testBitswap from './bitswap/index.js'
import testBlock from './block/index.js'
import testDag from './dag/index.js'
import testObject from './object/index.js'
import testPin from './pin/index.js'
import testBootstrap from './bootstrap/index.js'
import testDht from './dht/index.js'
import testName from './name/index.js'
import testNamePubsub from './name-pubsub/index.js'
import testPing from './ping/index.js'
import testPubsub from './pubsub/index.js'
import testSwarm from './swarm/index.js'
import testConfig from './config/index.js'
import testKey from './key/index.js'
import testMiscellaneous from './miscellaneous/index.js'
import testRepo from './repo/index.js'
import testStats from './stats/index.js'
export const root = createSuite({
add: testAdd,
addAll: testAddAll,
cat: testCat,
get: testGet,
ls: testLs,
refs: testRefs,
refsLocal: testRefsLocal
})
export const files = testFiles
export const bitswap = testBitswap
export const block = testBlock
export const dag = testDag
export const object = testObject
export const pin = testPin
export const bootstrap = testBootstrap
export const dht = testDht
export const name = testName
export const namePubsub = testNamePubsub
export const ping = testPing
export const pubsub = testPubsub
export const swarm = testSwarm
export const config = testConfig
export const key = testKey
export const miscellaneous = testMiscellaneous
export const repo = testRepo
export const stats = testStats
================================================
FILE: packages/interface-ipfs-core/src/key/gen.js
================================================
/* eslint-env mocha */
import { nanoid } from 'nanoid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { supportedKeys, importKey } from '@libp2p/crypto/keys'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testGen (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.key.gen', () => {
const keyTypes = [
{
opts: { type: 'rsa', size: 2048 },
expectedType: supportedKeys.rsa.RsaPrivateKey
},
{
opts: { type: 'ed25519' },
expectedType: supportedKeys.ed25519.Ed25519PrivateKey
},
{
opts: { },
expectedType: supportedKeys.ed25519.Ed25519PrivateKey
}
]
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
keyTypes.forEach((kt) => {
it(`should generate a new ${kt.opts.type || 'default'} key`, async function () {
this.timeout(20 * 1000)
const name = nanoid()
const key = await ipfs.key.gen(name, kt.opts)
expect(key).to.exist()
expect(key).to.have.property('name', name)
expect(key).to.have.property('id')
try {
const password = nanoid() + '-' + nanoid()
const exported = await ipfs.key.export(name, password)
const imported = await importKey(exported, password)
expect(imported).to.be.an.instanceOf(kt.expectedType)
} catch (/** @type {any} */ err) {
if (err.code === 'ERR_NOT_IMPLEMENTED') {
// key export is not exposed over the HTTP API
this.skip()
}
throw err
}
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/key/import.js
================================================
/* eslint-env mocha */
import { nanoid } from 'nanoid'
import { keys } from '@libp2p/crypto'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testImport (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.key.import', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should import an exported key', async () => {
const password = nanoid()
const key = await keys.generateKeyPair('Ed25519')
const exported = await key.export(password)
const importedKey = await ipfs.key.import('clone', exported, password)
expect(importedKey).to.exist()
expect(importedKey).to.have.property('name', 'clone')
expect(importedKey).to.have.property('id')
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/key/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testGen } from './gen.js'
import { testList } from './list.js'
import { testRename } from './rename.js'
import { testRm } from './rm.js'
import { testImport } from './import.js'
const tests = {
gen: testGen,
list: testList,
rename: testRename,
rm: testRm,
import: testImport
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/key/list.js
================================================
/* eslint-env mocha */
import { nanoid } from 'nanoid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testList (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.key.list', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should list all the keys', async function () {
this.timeout(60 * 1000)
const keys = await Promise.all([1, 2, 3].map(() => ipfs.key.gen(nanoid(), { type: 'rsa', size: 2048 })))
const res = await ipfs.key.list()
expect(res).to.exist()
expect(res).to.be.an('array')
expect(res.length).to.be.above(keys.length - 1)
keys.forEach(key => {
const found = res.find(({ id, name }) => name === key.name && id === key.id)
expect(found).to.exist()
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/key/rename.js
================================================
/* eslint-env mocha */
import { nanoid } from 'nanoid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testRename (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.key.rename', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should rename a key', async function () {
this.timeout(30 * 1000)
const oldName = nanoid()
const newName = nanoid()
const key = await ipfs.key.gen(oldName, { type: 'rsa', size: 2048 })
const renameRes = await ipfs.key.rename(oldName, newName)
expect(renameRes).to.exist()
expect(renameRes).to.have.property('was', oldName)
expect(renameRes).to.have.property('now', newName)
expect(renameRes).to.have.property('id', key.id)
const res = await ipfs.key.list()
expect(res.find(k => k.name === newName)).to.exist()
expect(res.find(k => k.name === oldName)).to.not.exist()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/key/rm.js
================================================
/* eslint-env mocha */
import { nanoid } from 'nanoid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testRm (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.key.rm', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should rm a key', async function () {
this.timeout(30 * 1000)
const key = await ipfs.key.gen(nanoid(), { type: 'rsa', size: 2048 })
const removeRes = await ipfs.key.rm(key.name)
expect(removeRes).to.exist()
expect(removeRes).to.have.property('name', key.name)
expect(removeRes).to.have.property('id', key.id)
const res = await ipfs.key.list()
expect(res.find(k => k.name === key.name)).to.not.exist()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/ls.js
================================================
/* eslint-env mocha */
import { fixtures } from './utils/index.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from './utils/mocha.js'
import all from 'it-all'
import { CID } from 'multiformats/cid'
import testTimeout from './utils/test-timeout.js'
/**
* @param {string} prefix
*/
const randomName = prefix => `${prefix}${Math.round(Math.random() * 1000)}`
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testLs (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.ls', function () {
this.timeout(120 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should respect timeout option when listing files', () => {
return testTimeout(() => ipfs.ls(CID.parse('QmNonExistentCiD8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXg'), {
timeout: 1
}))
})
it('should ls with a base58 encoded CID', async function () {
/**
* @param {string} name
*/
const content = (name) => ({
path: `test-folder/${name}`,
content: fixtures.directory.files[name]
})
/**
* @param {string} name
*/
const emptyDir = (name) => ({ path: `test-folder/${name}` })
const dirs = [
content('pp.txt'),
content('holmes.txt'),
content('jungle.txt'),
content('alice.txt'),
emptyDir('empty-folder'),
content('files/hello.txt'),
content('files/ipfs.txt'),
emptyDir('files/empty')
]
const res = await all(ipfs.addAll(dirs))
const root = res[res.length - 1]
expect(root.path).to.equal('test-folder')
expect(root.cid.toString()).to.equal(fixtures.directory.cid.toString())
const cid = 'QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP'
const output = await all(ipfs.ls(cid))
expect(output).to.have.lengthOf(6)
expect(output[0].name).to.equal('alice.txt')
expect(output[0].path).to.equal('QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/alice.txt')
expect(output[0].size).to.equal(11685)
expect(output[0].cid.toString()).to.equal('QmZyUEQVuRK3XV7L9Dk26pg6RVSgaYkiSTEdnT2kZZdwoi')
expect(output[0].type).to.equal('file')
expect(output[1].name).to.equal('empty-folder')
expect(output[1].path).to.equal('QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/empty-folder')
expect(output[1].size).to.equal(0)
expect(output[1].cid.toString()).to.equal('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')
expect(output[1].type).to.equal('dir')
expect(output[2].name).to.equal('files')
expect(output[2].path).to.equal('QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/files')
expect(output[2].size).to.equal(0)
expect(output[2].cid.toString()).to.equal('QmZ25UfTqXGz9RsEJFg7HUAuBcmfx5dQZDXQd2QEZ8Kj74')
expect(output[2].type).to.equal('dir')
expect(output[3].name).to.equal('holmes.txt')
expect(output[3].path).to.equal('QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/holmes.txt')
expect(output[3].size).to.equal(581878)
expect(output[3].cid.toString()).to.equal('QmR4nFjTu18TyANgC65ArNWp5Yaab1gPzQ4D8zp7Kx3vhr')
expect(output[3].type).to.equal('file')
expect(output[4].name).to.equal('jungle.txt')
expect(output[4].path).to.equal('QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/jungle.txt')
expect(output[4].size).to.equal(2294)
expect(output[4].cid.toString()).to.equal('QmT6orWioMiSqXXPGsUi71CKRRUmJ8YkuueV2DPV34E9y9')
expect(output[4].type).to.equal('file')
expect(output[5].name).to.equal('pp.txt')
expect(output[5].path).to.equal('QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP/pp.txt')
expect(output[5].size).to.equal(4540)
expect(output[5].cid.toString()).to.equal('QmVwdDCY4SPGVFnNCiZnX5CtzwWDn6kAM98JXzKxE3kCmn')
expect(output[5].type).to.equal('file')
})
it('should ls files added as CIDv0 with a CIDv1', async () => {
const dir = randomName('DIR')
const input = [
{ path: `${dir}/${randomName('F0')}`, content: randomName('D0') },
{ path: `${dir}/${randomName('F1')}`, content: randomName('D1') }
]
const res = await all(ipfs.addAll(input, { cidVersion: 0 }))
const cidv0 = res[res.length - 1].cid
expect(cidv0.version).to.equal(0)
const cidv1 = cidv0.toV1()
const output = await all(ipfs.ls(cidv1))
expect(output.length).to.equal(input.length)
output.forEach(({ cid }) => {
expect(res.find(file => file.cid.toString() === cid.toString())).to.exist()
})
})
it('should ls files added as CIDv1 with a CIDv0', async () => {
const dir = randomName('DIR')
const input = [
{ path: `${dir}/${randomName('F0')}`, content: randomName('D0') },
{ path: `${dir}/${randomName('F1')}`, content: randomName('D1') }
]
const res = await all(ipfs.addAll(input, { cidVersion: 1, rawLeaves: false }))
const cidv1 = res[res.length - 1].cid
expect(cidv1.version).to.equal(1)
const cidv0 = cidv1.toV1()
const output = await all(ipfs.ls(cidv0))
expect(output.length).to.equal(input.length)
output.forEach(({ cid }) => {
expect(res.find(file => file.cid.toString() === cid.toString())).to.exist()
})
})
it('should correctly handle a non existing hash', () => {
return expect(all(ipfs.ls('surelynotavalidhashheh?'))).to.eventually.be.rejected()
})
it('should correctly handle a non existing path', () => {
return expect(all(ipfs.ls('QmRNjDeKStKGTQXnJ2NFqeQ9oW/folder_that_isnt_there'))).to.eventually.be.rejected()
})
it('should ls files by path', async () => {
const dir = randomName('DIR')
const input = [
{ path: `${dir}/${randomName('F0')}`, content: randomName('D0') },
{ path: `${dir}/${randomName('F1')}`, content: randomName('D1') }
]
const res = await all(ipfs.addAll(input))
const output = await all(ipfs.ls(`/ipfs/${res[res.length - 1].cid}`))
expect(output.length).to.equal(input.length)
output.forEach(({ cid }) => {
expect(res.find(file => file.cid.toString() === cid.toString())).to.exist()
})
})
it('should ls with metadata', async () => {
const dir = randomName('DIR')
const mtime = new Date()
const mode = '0532'
const expectedMode = parseInt(mode, 8)
const expectedMtime = {
secs: Math.floor(mtime.getTime() / 1000),
nsecs: (mtime.getTime() - (Math.floor(mtime.getTime() / 1000) * 1000)) * 1000
}
const input = [
{ path: `${dir}/${randomName('F0')}`, content: randomName('D0'), mode, mtime },
{ path: `${dir}/${randomName('F1')}`, content: randomName('D1'), mode, mtime }
]
const res = await all(ipfs.addAll(input))
const output = await all(ipfs.ls(`/ipfs/${res[res.length - 1].cid}`))
expect(output).to.have.lengthOf(input.length)
expect(output[0].mtime).to.deep.equal(expectedMtime)
expect(output[0].mode).to.equal(expectedMode)
expect(output[1].mtime).to.deep.equal(expectedMtime)
expect(output[1].mode).to.equal(expectedMode)
})
it('should ls files by subdir', async () => {
const dir = randomName('DIR')
const subdir = randomName('F0')
const subfile = randomName('F1')
const input = { path: `${dir}/${subdir}/${subfile}`, content: randomName('D1') }
const res = await ipfs.add(input)
const path = `${res.cid}/${subdir}`
const output = await all(ipfs.ls(path))
expect(output).to.have.lengthOf(1)
expect(output[0]).to.have.property('path', `${path}/${subfile}`)
})
it('should ls single file', async () => {
const dir = randomName('DIR')
const file = randomName('F0')
const input = { path: `${dir}/${file}`, content: randomName('D1') }
const res = await ipfs.add(input)
const path = `${res.cid}/${file}`
const output = await all(ipfs.ls(path))
expect(output).to.have.lengthOf(1)
expect(output[0]).to.have.property('path', path)
})
it('should ls single file with metadata', async () => {
const dir = randomName('DIR')
const file = randomName('F0')
const input = {
path: `${dir}/${file}`,
content: randomName('D1'),
mode: 0o631,
mtime: {
secs: 5000,
nsecs: 100
}
}
const res = await ipfs.add(input)
const path = `${res.cid}/${file}`
const output = await all(ipfs.ls(res.cid))
expect(output).to.have.lengthOf(1)
expect(output[0]).to.have.property('path', path)
expect(output[0]).to.have.property('mode', input.mode)
expect(output[0]).to.have.deep.property('mtime', input.mtime)
})
it('should ls single file without containing directory', async () => {
const input = { content: randomName('D1') }
const res = await ipfs.add(input)
const output = await all(ipfs.ls(res.cid))
expect(output).to.have.lengthOf(1)
expect(output[0]).to.have.property('path', res.cid.toString())
})
it('should ls single file without containing directory with metadata', async () => {
const input = {
content: randomName('D1'),
mode: 0o631,
mtime: {
secs: 5000,
nsecs: 100
}
}
const res = await ipfs.add(input)
const output = await all(ipfs.ls(res.cid))
expect(output).to.have.lengthOf(1)
expect(output[0]).to.have.property('path', res.cid.toString())
expect(output[0]).to.have.property('mode', input.mode)
expect(output[0]).to.have.deep.property('mtime', input.mtime)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/miscellaneous/dns.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testDns (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.dns', function () {
this.timeout(60 * 1000)
this.retries(3)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should non-recursively resolve ipfs.io', async function () {
const domain = 'ipfs.io'
try {
const res = await ipfs.dns(domain, { recursive: false })
// matches pattern /ipns/
expect(res).to.match(/\/ipns\/.+$/)
} catch (/** @type {any} */ err) {
if (err.message.includes('could not resolve name')) {
return this.skip()
}
// happens when running tests offline
if (err.message.includes(`ECONNREFUSED ${domain}`)) {
return this.skip()
}
throw err
}
})
it('should recursively resolve ipfs.io', async function () {
const domain = 'ipfs.io'
try {
const res = await ipfs.dns(domain, { recursive: true })
// matches pattern /ipfs/
expect(res).to.match(/\/ipfs\/.+$/)
} catch (/** @type {any} */ err) {
if (err.message.includes('could not resolve name')) {
return this.skip()
}
// happens when running tests offline
if (err.message.includes(`ECONNREFUSED ${domain}`)) {
return this.skip()
}
throw err
}
})
it('should resolve subdomain docs.ipfs.io', async function () {
const domain = 'docs.ipfs.io'
try {
const res = await ipfs.dns(domain)
// matches pattern /ipfs/
expect(res).to.match(/\/ipfs\/.+$/)
} catch (/** @type {any} */ err) {
if (err.message.includes('could not resolve name')) {
return this.skip()
}
// happens when running tests offline
if (err.message.includes(`ECONNREFUSED ${domain}`)) {
return this.skip()
}
throw err
}
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/miscellaneous/id.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { isMultiaddr } from '@multiformats/multiaddr'
import { isWebWorker } from 'ipfs-utils/src/env.js'
import retry from 'p-retry'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testId (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.id', function () {
this.timeout(60 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get the node ID', async () => {
const res = await ipfs.id()
expect(res).to.have.a.property('id')
expect(res).to.have.a.property('publicKey')
expect(res).to.have.a.property('agentVersion').that.is.a('string')
expect(res).to.have.a.property('protocolVersion').that.is.a('string')
expect(res).to.have.a.property('addresses').that.is.an('array')
for (const ma of res.addresses) {
expect(isMultiaddr(ma)).to.be.true()
}
})
it('should have protocols property', async () => {
const res = await ipfs.id()
expect(res).to.have.a.property('protocols').that.is.an('array')
expect(res.protocols).to.include.members([
'/ipfs/bitswap/1.2.0',
'/ipfs/id/1.0.0',
'/ipfs/id/push/1.0.0',
'/ipfs/lan/kad/1.0.0',
'/ipfs/ping/1.0.0'
])
})
it('should return swarm ports opened after startup', async function () {
if (isWebWorker) {
// TODO: webworkers are not currently dialable
return this.skip()
}
await expect(ipfs.id()).to.eventually.have.property('addresses').that.is.not.empty()
})
it('should get the id of another node in the swarm', async function () {
if (isWebWorker) {
// TODO: https://github.com/libp2p/js-libp2p-websockets/issues/129
return this.skip()
}
const ipfsB = (await factory.spawn()).api
const ipfsBId = await ipfsB.id()
await ipfs.swarm.connect(ipfsBId.addresses[0])
// have to wait for identify to complete before protocols etc are available for remote hosts
await retry(async () => {
const result = await ipfs.id({
peerId: ipfsBId.id
})
expect(JSON.stringify(result, null, 2)).to.deep.equal(JSON.stringify(ipfsBId, null, 2))
}, { retries: 5 })
})
it('should get our own id when passed as an option', async function () {
const res = await ipfs.id()
const result = await ipfs.id({
peerId: res.id
})
expect(JSON.stringify(res, null, 2)).to.deep.equal(JSON.stringify(result, null, 2))
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/miscellaneous/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testId } from './id.js'
import { testVersion } from './version.js'
import { testStop } from './stop.js'
import { testResolve } from './resolve.js'
import { testDns } from './dns.js'
const tests = {
id: testId,
version: testVersion,
dns: testDns,
stop: testStop,
resolve: testResolve
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/miscellaneous/resolve.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import * as isIpfs from 'is-ipfs'
import { nanoid } from 'nanoid'
import { base64url } from 'multiformats/bases/base64'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import all from 'it-all'
import { isWebWorker } from 'ipfs-utils/src/env.js'
import merge from 'merge-options'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testResolve (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.resolve', function () {
this.timeout(60 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/** @type {import('ipfs-core-types/src/root').IDResult} */
let ipfsId
before(async () => {
ipfs = (await factory.spawn({
type: 'proc',
ipfsOptions: merge({
config: {
Routing: {
Type: 'none'
}
}
})
})).api
ipfsId = await ipfs.id()
})
after(() => factory.clean())
it('should resolve an IPFS hash', async () => {
const content = uint8ArrayFromString('Hello world')
const { cid } = await ipfs.add(content)
const path = await ipfs.resolve(`/ipfs/${cid}`)
expect(path).to.equal(`/ipfs/${cid}`)
})
it('should resolve an IPFS hash and return a base64url encoded CID in path', async () => {
const { cid } = await ipfs.add(uint8ArrayFromString('base64url encoded'), {
cidVersion: 1
})
const path = await ipfs.resolve(`/ipfs/${cid}`, { cidBase: 'base64url' })
const [,, cidStr] = path.split('/')
expect(cidStr).to.equal(cid.toString(base64url))
})
// Test resolve turns /ipfs/QmRootHash/path/to/file into /ipfs/QmFileHash
it('should resolve an IPFS path link', async () => {
const path = 'path/to/testfile.txt'
const content = uint8ArrayFromString('Hello world')
const [{ cid: fileCid }, , , { cid: rootCid }] = await all(ipfs.addAll([{ path, content }], { wrapWithDirectory: true }))
const resolve = await ipfs.resolve(`/ipfs/${rootCid}/${path}`)
expect(resolve).to.equal(`/ipfs/${fileCid}`)
})
it('should resolve up to the last node', async () => {
const content = { path: { to: { file: nanoid() } } }
const options = { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' }
const cid = await ipfs.dag.put(content, options)
const path = `/ipfs/${cid}/path/to/file`
const resolved = await ipfs.resolve(path)
expect(resolved).to.equal(path)
})
it('should resolve up to the last node across multiple nodes', async () => {
const options = { storeCodec: 'dag-cbor', hashAlg: 'sha2-256' }
const childCid = await ipfs.dag.put({ node: { with: { file: nanoid() } } }, options)
const parentCid = await ipfs.dag.put({ path: { to: childCid } }, options)
const resolved = await ipfs.resolve(`/ipfs/${parentCid}/path/to/node/with/file`)
expect(resolved).to.equal(`/ipfs/${childCid}/node/with/file`)
})
// Test resolve turns /ipns/domain.com into /ipfs/QmHash
it('should resolve an IPNS DNS link', async function () {
this.retries(3)
const domain = 'ipfs.io'
try {
const resolved = await ipfs.resolve(`/ipns/${domain}`)
expect(isIpfs.ipfsPath(resolved)).to.be.true()
} catch (/** @type {any} */ err) {
// happens when running tests offline
if (err.message.includes(`ECONNREFUSED ${domain}`)) {
return this.skip()
}
throw err
}
})
it('should resolve IPNS link recursively by default', async function () {
this.timeout(20 * 1000)
// webworkers are not dialable because webrtc is not available
const node = (await factory.spawn({
type: isWebWorker ? 'go' : undefined,
ipfsOptions: {
config: {
Routing: {
Type: 'none'
}
}
}
})).api
const nodeId = await node.id()
await ipfs.swarm.connect(nodeId.addresses[0])
const { path } = await ipfs.add(uint8ArrayFromString('should resolve a record recursive === true'))
const { id: keyId } = await ipfs.key.gen('key-name', { type: 'rsa', size: 2048 })
await ipfs.name.publish(path, { allowOffline: true })
await ipfs.name.publish(`/ipns/${ipfsId.id}`, { allowOffline: true, key: 'key-name', resolve: false })
return expect(ipfs.resolve(`/ipns/${keyId}`))
.to.eventually.equal(`/ipfs/${path}`)
})
it('should resolve IPNS link non-recursively if recursive==false', async function () {
this.timeout(20 * 1000)
// webworkers are not dialable because webrtc is not available
const node = (await factory.spawn({
type: isWebWorker ? 'go' : undefined,
ipfsOptions: {
config: {
Routing: {
Type: 'none'
}
}
}
})).api
const nodeId = await node.id()
await ipfs.swarm.connect(nodeId.addresses[0])
const { path } = await ipfs.add(uint8ArrayFromString('should resolve an IPNS key if recursive === false'))
const { id: keyId } = await ipfs.key.gen('new-key-name', { type: 'rsa', size: 2048 })
await ipfs.name.publish(path, { allowOffline: true })
await ipfs.name.publish(`/ipns/${ipfsId.id}`, { allowOffline: true, key: 'new-key-name', resolve: false })
return expect(ipfs.resolve(`/ipns/${keyId}`, { recursive: false }))
.to.eventually.equal(`/ipns/${ipfsId.id}`)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/miscellaneous/stop.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testStop (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.stop', function () {
this.timeout(60 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
beforeEach(async () => {
ipfs = (await factory.spawn()).api
})
afterEach(() => {
// reset the list of controlled nodes - we've already shut down the
// nodes started in this test but the references hang around and the
// next test will call `factory.clean()` which will explode when it
// can't connect to the nodes started by this test.
factory.controllers = []
})
it('should stop the node', async () => {
// Should succeed because node is started
await ipfs.swarm.peers()
// Stop the node and try the call again
await ipfs.stop()
// Trying to use an API that requires a started node should return an error
return expect(ipfs.swarm.peers()).to.eventually.be.rejected()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/miscellaneous/version.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testVersion (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.version', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get the node version', async () => {
const result = await ipfs.version()
expect(result).to.have.a.property('version')
expect(result).to.have.a.property('commit')
expect(result).to.have.a.property('repo')
})
it('should include the ipfs-http-client version', async () => {
const result = await ipfs.version()
expect(result).to.have.a.property('ipfs-http-client')
})
it('should include the interface-ipfs-core version', async () => {
const result = await ipfs.version()
expect(result).to.have.a.property('interface-ipfs-core')
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/name/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testPublish } from './publish.js'
import { testResolve } from './resolve.js'
const tests = {
publish: testPublish,
resolve: testResolve
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/name/publish.js
================================================
/* eslint-env mocha */
import { nanoid } from 'nanoid'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { fixture } from './utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import last from 'it-last'
import { peerIdFromString } from '@libp2p/peer-id'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
* @typedef {import('@libp2p/interface-peer-id').PeerId} PeerId
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testPublish (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.name.publish offline', () => {
const keyName = nanoid()
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/** @type {PeerId} */
let nodeId
before(async () => {
ipfs = (await factory.spawn({
ipfsOptions: {
config: {
Routing: {
Type: 'none'
}
}
}
})).api
const peerInfo = await ipfs.id()
nodeId = peerInfo.id
await ipfs.add(fixture.data, { pin: false })
})
after(() => factory.clean())
it('should publish an IPNS record with the default params', async function () {
this.timeout(50 * 1000)
const value = fixture.cid
const keys = await ipfs.key.list()
const self = keys.find(key => key.name === 'self')
if (!self) {
throw new Error('No self key found')
}
const res = await ipfs.name.publish(value, { allowOffline: true })
expect(res).to.exist()
expect(peerIdFromString(res.name).toString()).to.equal(peerIdFromString(self.id).toString())
expect(res.value).to.equal(`/ipfs/${value}`)
})
it('should publish correctly with the lifetime option and resolve', async () => {
const { path } = await ipfs.add(uint8ArrayFromString('should publish correctly with the lifetime option and resolve'))
await ipfs.name.publish(path, { allowOffline: true, resolve: false, lifetime: '2h' })
expect(await last(ipfs.name.resolve(`/ipns/${nodeId.toString()}`))).to.eq(`/ipfs/${path}`)
})
it('should publish correctly when the file was not added but resolve is disabled', async function () {
this.timeout(50 * 1000)
const value = 'QmPFVLPmp9zv5Z5KUqLhe2EivAGccQW2r7M7jhVJGLZoZU'
const keys = await ipfs.key.list()
const self = keys.find(key => key.name === 'self')
if (!self) {
throw new Error('No self key found')
}
const options = {
resolve: false,
lifetime: '1m',
ttl: '10s',
key: 'self',
allowOffline: true
}
const res = await ipfs.name.publish(value, options)
expect(res).to.exist()
expect(peerIdFromString(res.name).toString()).to.equal(peerIdFromString(self.id).toString())
expect(res.value).to.equal(`/ipfs/${value}`)
})
it('should publish with a key received as param, instead of using the key of the node', async function () {
this.timeout(90 * 1000)
const value = fixture.cid
const options = {
resolve: false,
lifetime: '24h',
ttl: '10s',
key: keyName,
allowOffline: true
}
const key = await ipfs.key.gen(keyName, { type: 'rsa', size: 2048 })
const res = await ipfs.name.publish(value, options)
expect(res).to.exist()
expect(peerIdFromString(res.name).toString()).to.equal(peerIdFromString(key.id).toString())
expect(res.value).to.equal(`/ipfs/${value}`)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/name/resolve.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import delay from 'delay'
import last from 'it-last'
import { CID } from 'multiformats/cid'
import * as Digest from 'multiformats/hashes/digest'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
* @typedef {import('@libp2p/interface-peer-id').PeerId} PeerId
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testResolve (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.name.resolve offline', function () {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/** @type {PeerId} */
let nodeId
before(async () => {
ipfs = (await factory.spawn({
ipfsOptions: {
config: {
Routing: {
Type: 'none'
}
}
}
})).api
const peerInfo = await ipfs.id()
nodeId = peerInfo.id
})
after(() => factory.clean())
it('should resolve a record default options', async function () {
this.timeout(20 * 1000)
const { path } = await ipfs.add(uint8ArrayFromString('should resolve a record default options'))
const { id: keyId } = await ipfs.key.gen('key-name-default', { type: 'rsa', size: 2048 })
await ipfs.name.publish(path, { allowOffline: true })
await ipfs.name.publish(`/ipns/${nodeId.toString()}`, { allowOffline: true, key: 'key-name-default' })
expect(await last(ipfs.name.resolve(`/ipns/${keyId}`)))
.to.eq(`/ipfs/${path}`)
})
it('should resolve a record from peerid as cidv1 in base32', async function () {
this.timeout(20 * 1000)
const { cid } = await ipfs.add(uint8ArrayFromString('should resolve a record from cidv1b32'))
const { id: peerId } = await ipfs.id()
await ipfs.name.publish(cid, { allowOffline: true })
// Represent Peer ID as CIDv1 Base32
// https://github.com/libp2p/specs/blob/master/RFC/0001-text-peerid-cid.md
const keyCid = CID.createV1(0x72, Digest.decode(peerId.toBytes()))
const resolvedPath = await last(ipfs.name.resolve(`/ipns/${keyCid}`))
expect(resolvedPath).to.equal(`/ipfs/${cid}`)
})
it('should resolve a record recursive === false', async () => {
const { path } = await ipfs.add(uint8ArrayFromString('should resolve a record recursive === false'))
await ipfs.name.publish(path, { allowOffline: true })
expect(await last(ipfs.name.resolve(`/ipns/${nodeId.toString()}`, { recursive: false })))
.to.eq(`/ipfs/${path}`)
})
it('should resolve a record recursive === true', async function () {
this.timeout(20 * 1000)
const { path } = await ipfs.add(uint8ArrayFromString('should resolve a record recursive === true'))
const { id: keyId } = await ipfs.key.gen('key-name', { type: 'rsa', size: 2048 })
await ipfs.name.publish(path, { allowOffline: true })
await ipfs.name.publish(`/ipns/${nodeId.toString()}`, { allowOffline: true, key: 'key-name' })
expect(await last(ipfs.name.resolve(`/ipns/${keyId}`, { recursive: true })))
.to.eq(`/ipfs/${path}`)
})
it('should resolve a record default options with remainder', async function () {
this.timeout(20 * 1000)
const { path } = await ipfs.add(uint8ArrayFromString('should resolve a record default options with remainder'))
const { id: keyId } = await ipfs.key.gen('key-name-remainder-default', { type: 'rsa', size: 2048 })
await ipfs.name.publish(path, { allowOffline: true })
await ipfs.name.publish(`/ipns/${nodeId.toString()}`, { allowOffline: true, key: 'key-name-remainder-default' })
expect(await last(ipfs.name.resolve(`/ipns/${keyId}/remainder/file.txt`)))
.to.eq(`/ipfs/${path}/remainder/file.txt`)
})
it('should resolve a record recursive === false with remainder', async () => {
const { path } = await ipfs.add(uint8ArrayFromString('should resolve a record recursive = false with remainder'))
await ipfs.name.publish(path, { allowOffline: true })
expect(await last(ipfs.name.resolve(`/ipns/${nodeId.toString()}/remainder/file.txt`, { recursive: false })))
.to.eq(`/ipfs/${path}/remainder/file.txt`)
})
it('should resolve a record recursive === true with remainder', async function () {
this.timeout(20 * 1000)
const { path } = await ipfs.add(uint8ArrayFromString('should resolve a record recursive = true with remainder'))
const { id: keyId } = await ipfs.key.gen('key-name-remainder', { type: 'rsa', size: 2048 })
await ipfs.name.publish(path, { allowOffline: true })
await ipfs.name.publish(`/ipns/${nodeId.toString()}`, { allowOffline: true, key: 'key-name-remainder' })
expect(await last(ipfs.name.resolve(`/ipns/${keyId}/remainder/file.txt`, { recursive: true })))
.to.eq(`/ipfs/${path}/remainder/file.txt`)
})
it('should not get the entry if its validity time expired', async () => {
const publishOptions = {
lifetime: '100ms',
ttl: '10s',
allowOffline: true
}
// we add new data instead of re-using fixture to make sure lifetime handling doesn't break
const { path } = await ipfs.add(uint8ArrayFromString('should not get the entry if its validity time expired'))
await ipfs.name.publish(path, publishOptions)
await delay(500)
// go only has 1 possible error https://github.com/ipfs/go-ipfs/blob/master/namesys/interface.go#L51
// so here we just expect an Error and don't match the error type to expiration
try {
await last(ipfs.name.resolve(nodeId))
} catch (/** @type {any} */ error) {
expect(error).to.exist()
}
})
})
describe('.name.resolve dns', function () {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
this.retries(5)
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should resolve /ipns/ipfs.io', async function () {
const domain = 'ipfs.io'
try {
expect(await last(ipfs.name.resolve(`/ipns/${domain}`)))
.to.match(/\/ipfs\/.+$/)
} catch (/** @type {any} */ err) {
// happens when running tests offline
if (err.message.includes(`ECONNREFUSED ${domain}`)) {
return this.skip()
}
throw err
}
})
it('should resolve /ipns/ipfs.io recursive === false', async function () {
const domain = 'ipfs.io'
try {
expect(await last(ipfs.name.resolve(`/ipns/${domain}`, { recursive: false })))
.to.match(/\/ipns\/.+$/)
} catch (/** @type {any} */ err) {
// happens when running tests offline
if (err.message.includes(`ECONNREFUSED ${domain}`)) {
return this.skip()
}
throw err
}
})
it('should resolve /ipns/ipfs.io recursive === true', async function () {
const domain = 'ipfs.io'
try {
expect(await last(ipfs.name.resolve(`/ipns/${domain}`, { recursive: true })))
.to.match(/\/ipfs\/.+$/)
} catch (/** @type {any} */ err) {
// happens when running tests offline
if (err.message.includes(`ECONNREFUSED ${domain}`)) {
return this.skip()
}
throw err
}
})
it('should resolve /ipns/ipfs.io with remainder', async function () {
const domain = 'ipfs.io'
try {
expect(await last(ipfs.name.resolve(`/ipns/${domain}/images/ipfs-logo.svg`)))
.to.match(/\/ipfs\/.+\/images\/ipfs-logo.svg$/)
} catch (/** @type {any} */ err) {
// happens when running tests offline
if (err.message.includes(`ECONNREFUSED ${domain}`)) {
return this.skip()
}
throw err
}
})
it('should resolve /ipns/ipfs.io with remainder recursive === false', async function () {
const domain = 'ipfs.io'
try {
expect(await last(ipfs.name.resolve(`/ipns/${domain}/images/ipfs-logo.svg`, { recursive: false })))
.to.match(/\/ipns\/.+\/images\/ipfs-logo.svg$/)
} catch (/** @type {any} */ err) {
// happens when running tests offline
if (err.message.includes(`ECONNREFUSED ${domain}`)) {
return this.skip()
}
throw err
}
})
it('should resolve /ipns/ipfs.io with remainder recursive === true', async function () {
const domain = 'ipfs.io'
try {
expect(await last(ipfs.name.resolve(`/ipns/${domain}/images/ipfs-logo.svg`, { recursive: true })))
.to.match(/\/ipfs\/.+\/images\/ipfs-logo.svg$/)
} catch (/** @type {any} */ err) {
// happens when running tests offline
if (err.message.includes(`ECONNREFUSED ${domain}`)) {
return this.skip()
}
throw err
}
})
it('should fail to resolve /ipns/ipfs.a', async () => {
try {
await last(ipfs.name.resolve('ipfs.a'))
} catch (/** @type {any} */ error) {
expect(error).to.exist()
}
})
it('should resolve ipns path with hamt-shard recursive === true', async function () {
const domain = 'tr.wikipedia-on-ipfs.org'
try {
expect(await last(ipfs.name.resolve(`/ipns/${domain}/wiki/Anasayfa.html`, { recursive: true })))
.to.match(/\/ipfs\/.+$/)
} catch (/** @type {any} */ err) {
// happens when running tests offline
if (err.message.includes(`ECONNREFUSED ${domain}`)) {
return this.skip()
}
throw err
}
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/name/utils.js
================================================
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
export const fixture = Object.freeze({
cid: 'Qma4hjFTnCasJ8PVp3mZbZK5g2vGDT4LByLJ7m8ciyRFZP',
data: uint8ArrayFromString('Plz add me!\n')
})
================================================
FILE: packages/interface-ipfs-core/src/name-pubsub/cancel.js
================================================
/* eslint-env mocha */
import all from 'it-all'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testCancel (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.name.pubsub.cancel', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/** @type {string} */
let nodeId
before(async () => {
ipfs = (await factory.spawn()).api
const peerInfo = await ipfs.id()
nodeId = peerInfo.id.toString()
})
after(() => factory.clean())
it('should return false when the name that is intended to cancel is not subscribed', async function () {
this.timeout(60 * 1000)
const res = await ipfs.name.pubsub.cancel(nodeId)
expect(res).to.exist()
expect(res).to.have.property('canceled')
expect(res.canceled).to.be.false()
})
it('should cancel a subscription correctly returning true', async function () {
this.timeout(300 * 1000)
const peerId = await createEd25519PeerId()
const id = peerId.toString()
const ipnsPath = `/ipns/${id}`
const subs = await ipfs.name.pubsub.subs()
expect(subs).to.be.an('array').that.does.not.include(ipnsPath)
await expect(all(ipfs.name.resolve(id))).to.eventually.be.rejected()
const subs1 = await ipfs.name.pubsub.subs()
const cancel = await ipfs.name.pubsub.cancel(ipnsPath)
const subs2 = await ipfs.name.pubsub.subs()
expect(subs1).to.be.an('array').that.includes(ipnsPath)
expect(cancel).to.have.property('canceled')
expect(cancel.canceled).to.be.true()
expect(subs2).to.be.an('array').that.does.not.include(ipnsPath)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/name-pubsub/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testCancel } from './cancel.js'
import { testState } from './state.js'
import { testSubs } from './subs.js'
import { testPubsub } from './pubsub.js'
const tests = {
cancel: testCancel,
state: testState,
subs: testSubs,
pubsub: testPubsub
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/name-pubsub/pubsub.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { peerIdFromString, peerIdFromKeys } from '@libp2p/peer-id'
import { isNode } from 'ipfs-utils/src/env.js'
import * as ipns from 'ipns'
import delay from 'delay'
import last from 'it-last'
import waitFor from '../utils/wait-for.js'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { ipnsValidator } from 'ipns/validator'
const namespace = '/record/'
const ipfsRef = '/ipfs/QmPFVLPmp9zv5Z5KUqLhe2EivAGccQW2r7M7jhVJGLZoZU'
const daemonsOptions = {
ipfsOptions: {
EXPERIMENTAL: {
ipnsPubsub: true
}
}
}
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
* @typedef {import('@libp2p/interface-pubsub').Message} Message
* @typedef {import('@libp2p/interfaces/events').EventHandler} EventHandler
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testPubsub (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.name.pubsub', () => {
// TODO make this work in the browser and between daemon and in-proc in nodes
if (!isNode) return
let nodes
/** @type {import('ipfs-core-types').IPFS} */
let nodeA
/** @type {import('ipfs-core-types').IPFS} */
let nodeB
/** @type {import('ipfs-core-types/src/root').IDResult} */
let idA
/** @type {import('ipfs-core-types/src/root').IDResult} */
let idB
before(async function () {
this.timeout(120 * 1000)
nodes = await Promise.all([
factory.spawn({ ...daemonsOptions }),
factory.spawn({ ...daemonsOptions })
])
nodeA = nodes[0].api
nodeB = nodes[1].api
const ids = await Promise.all([
nodeA.id(),
nodeB.id()
])
idA = ids[0]
idB = ids[1]
await nodeA.swarm.connect(idB.addresses[0])
await waitFor(async () => {
const res = await nodeB.swarm.peers()
return res.map(p => p.peer.toString()).includes(idA.id.toString())
}, { name: 'node A dialed node B' })
})
after(() => factory.clean())
it('should publish and then resolve correctly', async function () {
this.timeout(80 * 1000)
const routingKey = ipns.peerIdToRoutingKey(idA.id)
const topic = `${namespace}${uint8ArrayToString(routingKey, 'base64url')}`
await nodeB.pubsub.subscribe(topic, () => {})
// wait for nodeA to see nodeB's subscription
await waitFor(async () => {
const peers = await nodeA.pubsub.peers(topic)
return peers.map(p => p.toString()).includes(idB.id.toString())
})
await nodeA.name.publish(ipfsRef, { resolve: false })
await delay(1000) // guarantee record is written
const res = await last(nodeB.name.resolve(idA.id))
expect(res).to.equal(ipfsRef)
})
it('should self resolve, publish and then resolve correctly', async function () {
this.timeout(6000)
const emptyDirCid = '/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn'
const { path } = await nodeA.add(uint8ArrayFromString('pubsub records'))
const resolvesEmpty = await last(nodeB.name.resolve(idB.id))
expect(resolvesEmpty).to.be.eq(emptyDirCid)
const publish = await nodeB.name.publish(path)
expect(publish).to.be.eql({
name: idB.id.toString(),
value: `/ipfs/${path}`
})
const resolveB = await last(nodeB.name.resolve(idB.id))
expect(resolveB).to.be.eq(`/ipfs/${path}`)
await delay(1000)
const resolveA = await last(nodeA.name.resolve(idB.id))
expect(resolveA).to.be.eq(`/ipfs/${path}`)
})
it('should handle event on publish correctly', async function () {
this.timeout(80 * 1000)
const testAccountName = 'test-account'
/**
* @type {import('@libp2p/interface-pubsub').Message}
*/
let publishedMessage
/**
* @type {EventHandler}
*/
const checkMessage = (msg) => {
publishedMessage = msg
}
const alreadySubscribed = () => {
return Boolean(publishedMessage)
}
// Create account for publish
const testAccount = await nodeA.key.gen(testAccountName, {
type: 'rsa',
size: 2048,
'ipns-base': 'b58mh'
})
const routingKey = ipns.peerIdToRoutingKey(peerIdFromString(testAccount.id))
const topic = `${namespace}${uint8ArrayToString(routingKey, 'base64url')}`
await nodeB.pubsub.subscribe(topic, checkMessage)
// wait for nodeA to see nodeB's subscription
await waitFor(async () => {
const peers = await nodeA.pubsub.peers(topic)
return peers.map(p => p.toString()).includes(idB.id.toString())
})
await nodeA.name.publish(ipfsRef, { resolve: false, key: testAccountName })
await waitFor(alreadySubscribed)
// @ts-expect-error publishedMessage is set in handler
if (!publishedMessage) {
throw new Error('Pubsub handler not invoked')
}
const publishedMessageData = ipns.unmarshal(publishedMessage.data)
if (publishedMessage.type !== 'signed') {
throw new Error('Message was not signed')
}
if (publishedMessageData.pubKey == null) {
throw new Error('Public key was missing from published message data')
}
const messageKey = publishedMessage.from
const pubKeyPeerId = await peerIdFromKeys(publishedMessageData.pubKey)
expect(pubKeyPeerId.toString()).not.to.equal(messageKey.toString())
expect(pubKeyPeerId.toString()).to.equal(testAccount.id)
expect(publishedMessage.from.toString()).to.equal(idA.id.toString())
expect(messageKey.toString()).to.equal(idA.id.toString())
expect(uint8ArrayToString(publishedMessageData.value)).to.equal(ipfsRef)
// Verify the signature
await ipnsValidator(ipns.peerIdToRoutingKey(pubKeyPeerId), publishedMessage.data)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/name-pubsub/state.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testState (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.name.pubsub.state', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get the current state of pubsub', async function () {
this.timeout(50 * 1000)
const res = await ipfs.name.pubsub.state()
expect(res).to.exist()
expect(res).to.have.property('enabled')
expect(res.enabled).to.be.eql(true)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/name-pubsub/subs.js
================================================
/* eslint-env mocha */
import all from 'it-all'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testSubs (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.name.pubsub.subs', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get an empty array as a result of subscriptions before any resolve', async function () {
this.timeout(60 * 1000)
const res = await ipfs.name.pubsub.subs()
expect(res).to.exist()
expect(res).to.eql([])
})
it('should get the list of subscriptions updated after a resolve', async function () {
this.timeout(300 * 1000)
const id = 'QmNP1ASen5ZREtiJTtVD3jhMKhoPb1zppET1tgpjHx2NGA'
const subs = await ipfs.name.pubsub.subs()
expect(subs).to.eql([]) // initally empty
await expect(all(ipfs.name.resolve(id))).to.eventually.be.rejected()
const res = await ipfs.name.pubsub.subs()
expect(res).to.be.an('array').that.does.include(`/ipns/${id}`)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/object/data.js
================================================
/* eslint-env mocha */
import { nanoid } from 'nanoid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testData (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.object.data', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get data by CID', async () => {
const testObj = {
Data: uint8ArrayFromString(nanoid()),
Links: []
}
const nodeCid = await ipfs.object.put(testObj)
const data = await ipfs.object.data(nodeCid)
expect(testObj.Data).to.equalBytes(data)
})
it('returns error for request without argument', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.data(null)).to.eventually.be.rejected.and.be.an.instanceOf(Error)
})
it('returns error for request with invalid argument', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.data('invalid', { enc: 'base58' })).to.eventually.be.rejected.and.be.an.instanceOf(Error)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/object/get.js
================================================
/* eslint-env mocha */
import * as dagPB from '@ipld/dag-pb'
import { nanoid } from 'nanoid'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { UnixFS } from 'ipfs-unixfs'
import { randomBytes } from 'iso-random-stream'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { CID } from 'multiformats/cid'
import { sha256 } from 'multiformats/hashes/sha2'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testGet (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.object.get', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get object by multihash', async () => {
const obj = {
Data: uint8ArrayFromString(nanoid()),
Links: []
}
const node1Cid = await ipfs.object.put(obj)
const node1 = await ipfs.object.get(node1Cid)
let node2 = await ipfs.object.get(node1Cid)
// because js-ipfs-api can't infer if the
// returned Data is Uint8Array or String
if (typeof node2.Data === 'string') {
node2 = {
Data: uint8ArrayFromString(node2.Data),
Links: node2.Links
}
}
expect(node1.Data).to.eql(node2.Data)
expect(node1.Links).to.eql(node2.Links)
})
it('should get object with links by multihash string', async () => {
const node1a = {
Data: uint8ArrayFromString('Some data 1'),
Links: []
}
const node2 = {
Data: uint8ArrayFromString('Some data 2'),
Links: []
}
const node2Buf = dagPB.encode(node2)
const link = {
Name: 'some-link',
Tsize: node2Buf.length,
Hash: CID.createV0(await sha256.digest(node2Buf))
}
const node1b = {
Data: node1a.Data,
Links: [link]
}
const node1bCid = await ipfs.object.put(node1b)
let node1c = await ipfs.object.get(node1bCid)
// because js-ipfs-api can't infer if the
// returned Data is Uint8Array or String
if (typeof node1c.Data === 'string') {
node1c = {
Data: uint8ArrayFromString(node1c.Data),
Links: node1c.Links
}
}
expect(node1a.Data).to.eql(node1c.Data)
})
it('should get object by base58 encoded multihash', async () => {
const obj = {
Data: uint8ArrayFromString(nanoid()),
Links: []
}
const node1aCid = await ipfs.object.put(obj)
const node1a = await ipfs.object.get(node1aCid)
let node1b = await ipfs.object.get(node1aCid, { enc: 'base58' })
// because js-ipfs-api can't infer if the
// returned Data is Uint8Array or String
if (typeof node1b.Data === 'string') {
node1b = {
Data: uint8ArrayFromString(node1b.Data),
Links: node1b.Links
}
}
expect(node1a.Data).to.eql(node1b.Data)
expect(node1a.Links).to.eql(node1b.Links)
})
it('should supply unaltered data', async () => {
// has to be big enough to span several DAGNodes
const data = randomBytes(1024 * 3000)
const result = await ipfs.add({
path: '',
content: data
})
const node = await ipfs.object.get(result.cid)
if (!node.Data) {
throw new Error('Node did not have data')
}
const meta = UnixFS.unmarshal(node.Data)
expect(meta.fileSize()).to.equal(data.length)
})
it('should error for request without argument', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.get(null)).to.eventually.be.rejected.and.be.an.instanceOf(Error)
})
it('returns error for request with invalid argument', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.get('invalid', { enc: 'base58' })).to.eventually.be.rejected.and.be.an.instanceOf(Error)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/object/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testNew } from './new.js'
import { testPut } from './put.js'
import { testGet } from './get.js'
import { testData } from './data.js'
import { testLinks } from './links.js'
import { testStat } from './stat.js'
import testPatch from './patch/index.js'
const tests = {
new: testNew,
put: testPut,
get: testGet,
data: testData,
links: testLinks,
stat: testStat,
patch: testPatch
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/object/links.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import * as dagPB from '@ipld/dag-pb'
import { nanoid } from 'nanoid'
import { CID } from 'multiformats/cid'
import { sha256 } from 'multiformats/hashes/sha2'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testLinks (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.object.links', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get empty links by multihash', async () => {
const testObj = {
Data: uint8ArrayFromString(nanoid()),
Links: []
}
const cid = await ipfs.object.put(testObj)
const node = await ipfs.object.get(cid)
const links = await ipfs.object.links(cid)
expect(node.Links).to.eql(links)
})
it('should get links by multihash', async () => {
const node1a = {
Data: uint8ArrayFromString('Some data 1'),
Links: []
}
const node2 = {
Data: uint8ArrayFromString('Some data 2'),
Links: []
}
const node2Buf = dagPB.encode(node2)
const link = {
Name: 'some-link',
Tsize: node2Buf.length,
Hash: CID.createV0(await sha256.digest(node2Buf))
}
const node1b = {
Data: node1a.Data,
Links: [link]
}
const node1bCid = await ipfs.object.put(node1b)
const links = await ipfs.object.links(node1bCid)
expect(links).to.have.lengthOf(1)
expect(node1b.Links).to.deep.equal(links)
})
it('should get links from CBOR object', async () => {
const hashes = []
const res1 = await ipfs.add(uint8ArrayFromString('test data'))
hashes.push(res1.cid)
const res2 = await ipfs.add(uint8ArrayFromString('more test data'))
hashes.push(res2.cid)
const obj = {
some: 'data',
mylink: hashes[0],
myobj: {
anotherLink: hashes[1]
}
}
const cid = await ipfs.dag.put(obj)
const links = await ipfs.object.links(cid)
expect(links.length).to.eql(2)
// TODO: js-ipfs succeeds but go returns empty strings for link name
// const names = [links[0].name, links[1].name]
// expect(names).includes('mylink')
// expect(names).includes('myobj/anotherLink')
const cids = [links[0].Hash.toString(), links[1].Hash.toString()]
expect(cids).includes(hashes[0].toString())
expect(cids).includes(hashes[1].toString())
})
it('returns error for request without argument', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.links(null)).to.eventually.be.rejected.and.be.an.instanceOf(Error)
})
it('returns error for request with invalid argument', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.links('invalid', { enc: 'base58' })).to.eventually.be.rejected.and.be.an.instanceOf(Error)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/object/new.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testNew (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.object.new', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should create a new object with no template', async () => {
const cid = await ipfs.object.new()
expect(cid.toString()).to.equal('QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n')
})
it('should create a new object with unixfs-dir template', async () => {
const cid = await ipfs.object.new({ template: 'unixfs-dir' })
expect(cid.toString()).to.equal('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/object/patch/add-link.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import * as dagPB from '@ipld/dag-pb'
import { CID } from 'multiformats/cid'
import { sha256 } from 'multiformats/hashes/sha2'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testAddLink (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.object.patch.addLink', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should add a link to an existing node', async () => {
const obj = {
Data: uint8ArrayFromString('patch test object'),
Links: []
}
// link to add
const node2 = {
Data: uint8ArrayFromString('some other node'),
Links: []
}
// note: we need to put the linked obj, otherwise IPFS won't
// timeout. Reason: it needs the node to get its size
await ipfs.object.put(node2)
const node2Buf = dagPB.encode(node2)
const link = {
Name: 'link-to-node',
Tsize: node2Buf.length,
Hash: CID.createV0(await sha256.digest(node2Buf))
}
// manual create dag step by step
const node1a = {
Data: obj.Data,
Links: obj.Links
}
const node1b = {
Data: node1a.Data,
Links: [link]
}
const node1bCid = await ipfs.object.put(node1b)
// add link with patch.addLink
const testNodeCid = await ipfs.object.put(obj)
const cid = await ipfs.object.patch.addLink(testNodeCid, link)
// assert both are equal
expect(node1bCid).to.eql(cid)
/* TODO: revisit this assertions.
// note: make sure we can link js plain objects
const content = uint8ArrayFromString(JSON.stringify({
title: 'serialized object'
}, null, 0))
const result = await ipfs.add(content)
expect(result).to.exist()
expect(result).to.have.lengthOf(1)
const object = result.pop()
const node3 = {
name: object.hash,
multihash: object.hash,
size: object.size
}
const node = await ipfs.object.patch.addLink(testNodeWithLinkMultihash, node3)
expect(node).to.exist()
testNodeWithLinkMultihash = node.multihash
testLinkPlainObject = node3
*/
})
it('returns error for request without arguments', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.patch.addLink(null, null, null)).to.eventually.be.rejected.and.be.an.instanceOf(Error)
})
it('returns error for request with only one invalid argument', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.patch.addLink('invalid', null, null)).to.eventually.be.rejected.and.be.an.instanceOf(Error)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/object/patch/append-data.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testAppendData (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.object.patch.appendData', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should append data to an existing node', async () => {
const obj = {
Data: uint8ArrayFromString('patch test object'),
Links: []
}
const nodeCid = await ipfs.object.put(obj)
const patchedNodeCid = await ipfs.object.patch.appendData(nodeCid, uint8ArrayFromString('append'))
expect(patchedNodeCid).to.not.deep.equal(nodeCid)
})
it('returns error for request without key & data', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.patch.appendData(null, null)).to.eventually.be.rejected.and.be.an.instanceOf(Error)
})
it('returns error for request without data', () => {
const filePath = 'test/fixtures/test-data/badnode.json'
// @ts-expect-error invalid arg
return expect(ipfs.object.patch.appendData(null, filePath)).to.eventually.be.rejected.and.be.an.instanceOf(Error)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/object/patch/index.js
================================================
import { createSuite } from '../../utils/suite.js'
import { testAddLink } from './add-link.js'
import { testRmLink } from './rm-link.js'
import { testAppendData } from './append-data.js'
import { testSetData } from './set-data.js'
const tests = {
addLink: testAddLink,
rmLink: testRmLink,
appendData: testAppendData,
setData: testSetData
}
export default createSuite(tests, 'patch')
================================================
FILE: packages/interface-ipfs-core/src/object/patch/rm-link.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import * as dagPB from '@ipld/dag-pb'
import { CID } from 'multiformats/cid'
import { sha256 } from 'multiformats/hashes/sha2'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testRmLink (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.object.patch.rmLink', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should remove a link from an existing node', async () => {
const obj1 = {
Data: uint8ArrayFromString('patch test object 1'),
Links: []
}
const obj2 = {
Data: uint8ArrayFromString('patch test object 2'),
Links: []
}
const nodeCid = await ipfs.object.put(obj1)
const childCid = await ipfs.object.put(obj2)
const child = await ipfs.object.get(childCid)
const childBuf = dagPB.encode(child)
const childAsDAGLink = {
Name: 'my-link',
Tsize: childBuf.length,
Hash: CID.createV0(await sha256.digest(childBuf))
}
const parentCid = await ipfs.object.patch.addLink(nodeCid, childAsDAGLink)
const withoutChildCid = await ipfs.object.patch.rmLink(parentCid, childAsDAGLink)
expect(withoutChildCid).to.not.deep.equal(parentCid)
expect(withoutChildCid).to.deep.equal(nodeCid)
/* TODO: revisit this assertions.
const node = await ipfs.object.patch.rmLink(testNodeWithLinkMultihash, testLinkPlainObject)
expect(node.multihash).to.not.deep.equal(testNodeWithLinkMultihash)
*/
})
it('returns error for request without arguments', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.patch.rmLink(null, null)).to.eventually.be.rejected
.and.be.an.instanceOf(Error)
})
it('returns error for request only one invalid argument', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.patch.rmLink('invalid', null)).to.eventually.be.rejected
.and.be.an.instanceOf(Error)
})
it('returns error for request with invalid first argument', () => {
const root = ''
const link = 'foo'
// @ts-expect-error invalid arg
return expect(ipfs.object.patch.rmLink(root, link)).to.eventually.be.rejected
.and.be.an.instanceOf(Error)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/object/patch/set-data.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testSetData (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.object.patch.setData', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should set data for an existing node', async () => {
const obj = {
Data: uint8ArrayFromString('patch test object'),
Links: []
}
const patchData = uint8ArrayFromString('set')
const nodeCid = await ipfs.object.put(obj)
const patchedNodeCid = await ipfs.object.patch.setData(nodeCid, patchData)
const patchedNode = await ipfs.object.get(patchedNodeCid)
expect(nodeCid).to.not.deep.equal(patchedNodeCid)
expect(patchedNode.Data).to.eql(patchData)
})
it('returns error for request without key & data', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.patch.setData(null, null)).to.eventually.be.rejected.and.be.an.instanceOf(Error)
})
it('returns error for request without data', () => {
const filePath = 'test/fixtures/test-data/badnode.json'
// @ts-expect-error invalid arg
return expect(ipfs.object.patch.setData(null, filePath)).to.eventually.be.rejected.and.be.an.instanceOf(Error)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/object/put.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import * as dagPB from '@ipld/dag-pb'
import { nanoid } from 'nanoid'
import { CID } from 'multiformats/cid'
import { sha256 } from 'multiformats/hashes/sha2'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import first from 'it-first'
import drain from 'it-drain'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testPut (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.object.put', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should put an object', async () => {
const obj = {
Data: uint8ArrayFromString(nanoid()),
Links: []
}
const cid = await ipfs.object.put(obj)
const node = await ipfs.object.get(cid)
expect(node).to.deep.equal(obj)
})
it('should pin an object when putting', async () => {
const obj = {
Data: uint8ArrayFromString(nanoid()),
Links: []
}
const cid = await ipfs.object.put(obj, {
pin: true
})
const pin = await first(ipfs.pin.ls({
paths: cid
}))
expect(pin).to.have.deep.property('cid', cid)
expect(pin).to.have.property('type', 'recursive')
})
it('should not pin an object by default', async () => {
const obj = {
Data: uint8ArrayFromString(nanoid()),
Links: []
}
const cid = await ipfs.object.put(obj)
return expect(drain(ipfs.pin.ls({
paths: cid
}))).to.eventually.be.rejectedWith(/not pinned/)
})
it('should put a Protobuf DAGNode', async () => {
const dNode = {
Data: uint8ArrayFromString(nanoid()),
Links: []
}
const cid = await ipfs.object.put(dNode)
const node = await ipfs.object.get(cid)
expect(dNode).to.deep.equal(node)
})
it('should fail if a string is passed', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.put(nanoid())).to.eventually.be.rejected()
})
it('should put a Protobuf DAGNode with a link', async () => {
const node1a = {
Data: uint8ArrayFromString(nanoid()),
Links: []
}
const node2 = {
Data: uint8ArrayFromString(nanoid()),
Links: []
}
const node2Buf = dagPB.encode(node2)
const link = {
Name: 'some-link',
Tsize: node2Buf.length,
Hash: CID.createV0(await sha256.digest(node2Buf))
}
const node1b = {
Data: node1a.Data,
Links: [link]
}
const cid = await ipfs.object.put(node1b)
const node = await ipfs.object.get(cid)
expect(node1b.Data).to.deep.equal(node.Data)
expect(node1b.Links).to.deep.equal(node.Links)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/object/stat.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import * as dagPB from '@ipld/dag-pb'
import { nanoid } from 'nanoid'
import { CID } from 'multiformats/cid'
import { sha256 } from 'multiformats/hashes/sha2'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testStat (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.object.stat', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get stats by multihash', async () => {
const testObj = {
Data: uint8ArrayFromString('get test object'),
Links: []
}
const cid = await ipfs.object.put(testObj)
const stats = await ipfs.object.stat(cid)
const expected = {
Hash: CID.parse('QmNggDXca24S6cMPEYHZjeuc4QRmofkRrAEqVL3Ms2sdJZ').toV1(),
NumLinks: 0,
BlockSize: 17,
LinksSize: 2,
DataSize: 15,
CumulativeSize: 17
}
expect(stats).to.deep.equal(expected)
})
it('should get stats for object with links by multihash', async () => {
const node1a = {
Data: uint8ArrayFromString(nanoid()),
Links: []
}
const node2 = {
Data: uint8ArrayFromString(nanoid()),
Links: []
}
const node2Buf = dagPB.encode(node2)
const link = {
Name: 'some-link',
Tsize: node2Buf.length,
Hash: CID.createV0(await sha256.digest(node2Buf))
}
const node1b = {
Data: node1a.Data,
Links: [link]
}
const node1bCid = await ipfs.object.put(node1b)
const stats = await ipfs.object.stat(node1bCid)
const expected = {
Hash: node1bCid,
NumLinks: 1,
BlockSize: 74,
LinksSize: 53,
DataSize: 21,
CumulativeSize: 97
}
expect(stats).to.deep.equal(expected)
})
it('returns error for request without argument', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.stat(null)).to.eventually.be.rejected.and.be.an.instanceOf(Error)
})
it('returns error for request with invalid argument', () => {
// @ts-expect-error invalid arg
return expect(ipfs.object.stat('invalid', { enc: 'base58' })).to.eventually.be.rejected.and.be.an.instanceOf(Error)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/pin/add-all.js
================================================
/* eslint-env mocha */
import { fixtures, clearPins } from './utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import all from 'it-all'
import drain from 'it-drain'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testAddAll (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.pin.addAll', function () {
this.timeout(50 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
await drain(
ipfs.addAll(
fixtures.files.map(file => ({ content: file.data })), {
pin: false
}
)
)
await drain(
ipfs.addAll(fixtures.directory.files.map(
file => ({
path: file.path,
content: file.data
})
), {
pin: false
})
)
})
after(() => factory.clean())
beforeEach(() => {
return clearPins(ipfs)
})
/**
*
* @param {Iterable | AsyncIterable} source
*/
async function testAddPinInput (source) {
const pinset = await all(ipfs.pin.addAll(source))
expect(pinset).to.have.deep.members([
fixtures.files[0].cid,
fixtures.files[1].cid
])
}
it('should add an array of CIDs', () => {
return testAddPinInput([
fixtures.files[0].cid,
fixtures.files[1].cid
])
})
it('should add a generator of CIDs', () => {
return testAddPinInput(function * () {
yield fixtures.files[0].cid
yield fixtures.files[1].cid
}())
})
it('should add an async generator of CIDs', () => {
return testAddPinInput(async function * () { // eslint-disable-line require-await
yield fixtures.files[0].cid
yield fixtures.files[1].cid
}())
})
it('should add an array of pins with options', () => {
return testAddPinInput([
{
cid: fixtures.files[0].cid,
recursive: false
},
{
cid: fixtures.files[1].cid,
recursive: true
}
])
})
it('should add a generator of pins with options', () => {
return testAddPinInput(function * () {
yield {
cid: fixtures.files[0].cid,
recursive: false
}
yield {
cid: fixtures.files[1].cid,
recursive: true
}
}())
})
it('should add an async generator of pins with options', () => {
return testAddPinInput(async function * () { // eslint-disable-line require-await
yield {
cid: fixtures.files[0].cid,
recursive: false
}
yield {
cid: fixtures.files[1].cid,
recursive: true
}
}())
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/pin/add.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { fixtures, clearPins, expectPinned, expectNotPinned, pinTypes } from './utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import all from 'it-all'
import drain from 'it-drain'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testAdd (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.pin.add', function () {
this.timeout(50 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
await drain(
ipfs.addAll(
fixtures.files.map(file => ({ content: file.data })), {
pin: false
}
)
)
await drain(
ipfs.addAll(fixtures.directory.files.map(
file => ({
path: file.path,
content: file.data
})
), {
pin: false
})
)
})
after(() => factory.clean())
beforeEach(() => {
return clearPins(ipfs)
})
it('should add a CID and return the added CID', async () => {
const cid = await ipfs.pin.add(fixtures.files[0].cid)
expect(cid).to.deep.equal(fixtures.files[0].cid)
})
it('should add a pin with options and return the added CID', async () => {
const cid = await ipfs.pin.add(fixtures.files[0].cid, {
recursive: false
})
expect(cid).to.deep.equal(fixtures.files[0].cid)
})
it('should add recursively', async () => {
await ipfs.pin.add(fixtures.directory.cid)
await expectPinned(ipfs, fixtures.directory.cid, pinTypes.recursive)
const pinChecks = Object.values(fixtures.directory.files).map(file => expectPinned(ipfs, file.cid))
return Promise.all(pinChecks)
})
it('should add directly', async () => {
await ipfs.pin.add(fixtures.directory.cid, {
recursive: false
})
await expectPinned(ipfs, fixtures.directory.cid, pinTypes.direct)
await expectNotPinned(ipfs, fixtures.directory.files[0].cid)
})
it('should recursively pin parent of direct pin', async () => {
await ipfs.pin.add(fixtures.directory.files[0].cid, {
recursive: false
})
await ipfs.pin.add(fixtures.directory.cid)
// file is pinned both directly and indirectly o.O
await expectPinned(ipfs, fixtures.directory.files[0].cid, pinTypes.direct)
await expectPinned(ipfs, fixtures.directory.files[0].cid, pinTypes.indirect)
})
it('should fail to directly pin a recursive pin', async () => {
await ipfs.pin.add(fixtures.directory.cid)
return expect(ipfs.pin.add(fixtures.directory.cid, {
recursive: false
}))
.to.eventually.be.rejectedWith(/already pinned recursively/)
})
it('should fail to pin a hash not in datastore', async function () {
this.slow(3 * 1000)
this.timeout(5 * 1000)
const falseHash = `${`${fixtures.directory.cid}`.slice(0, -2)}ss`
await expect(ipfs.pin.add(falseHash, { timeout: '2s' }))
.to.eventually.be.rejected().with.property('name', 'TimeoutError')
})
it('needs all children in datastore to pin recursively', async function () {
this.slow(3 * 1000)
this.timeout(5 * 1000)
await all(ipfs.block.rm(fixtures.directory.files[0].cid))
await expect(ipfs.pin.add(fixtures.directory.cid, { timeout: '2s' }))
.to.eventually.be.rejected().with.property('name', 'TimeoutError')
})
it('should pin dag-cbor', async () => {
const cid = await ipfs.dag.put({}, {
storeCodec: 'dag-cbor',
hashAlg: 'sha2-256'
})
await ipfs.pin.add(cid)
const pins = await all(ipfs.pin.ls())
expect(pins).to.deep.include({
type: 'recursive',
cid
})
})
it('should pin raw', async () => {
const cid = await ipfs.dag.put(new Uint8Array(0), {
storeCodec: 'raw',
hashAlg: 'sha2-256'
})
await ipfs.pin.add(cid)
const pins = await all(ipfs.pin.ls())
expect(pins).to.deep.include({
type: 'recursive',
cid
})
})
it('should pin dag-cbor with dag-pb child', async () => {
const child = await ipfs.dag.put({
Data: uint8ArrayFromString(`${Math.random()}`),
Links: []
}, {
storeCodec: 'dag-pb',
hashAlg: 'sha2-256'
})
const parent = await ipfs.dag.put({
child
}, {
storeCodec: 'dag-cbor',
hashAlg: 'sha2-256'
})
await ipfs.pin.add(parent, {
recursive: true
})
const pins = await all(ipfs.pin.ls())
expect(pins).to.deep.include({
cid: parent,
type: 'recursive'
})
expect(pins).to.deep.include({
cid: child,
type: 'indirect'
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/pin/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testAdd } from './add.js'
import { testAddAll } from './add-all.js'
import { testLs } from './ls.js'
import { testRm } from './rm.js'
import { testRmAll } from './rm-all.js'
import testRemote from './remote/index.js'
const tests = {
add: testAdd,
addAll: testAddAll,
ls: testLs,
rm: testRm,
rmAll: testRmAll,
remote: testRemote
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/pin/ls.js
================================================
/* eslint-env mocha */
import { fixtures } from './utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import all from 'it-all'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testLs (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.pin.ls', function () {
this.timeout(50 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
// two files wrapped in directories, only root CID pinned recursively
const dir = fixtures.directory.files.map((file) => ({ path: file.path, content: file.data }))
await all(ipfs.addAll(dir, { pin: false, cidVersion: 0 }))
await ipfs.pin.add(fixtures.directory.cid, { recursive: true })
// a file (CID pinned recursively)
await ipfs.add(fixtures.files[0].data, { pin: false, cidVersion: 0 })
await ipfs.pin.add(fixtures.files[0].cid, { recursive: true })
// a single CID (pinned directly)
await ipfs.add(fixtures.files[1].data, { pin: false, cidVersion: 0 })
await ipfs.pin.add(fixtures.files[1].cid, { recursive: false })
})
after(() => factory.clean())
// 1st, because ipfs.add pins automatically
it('should list all recursive pins', async () => {
const pinset = await all(ipfs.pin.ls({ type: 'recursive' }))
expect(pinset).to.deep.include({
type: 'recursive',
cid: fixtures.files[0].cid
})
expect(pinset).to.deep.include({
type: 'recursive',
cid: fixtures.directory.cid
})
})
it('should list all indirect pins', async () => {
const pinset = await all(ipfs.pin.ls({ type: 'indirect' }))
expect(pinset).to.not.deep.include({
type: 'recursive',
cid: fixtures.files[0].cid
})
expect(pinset).to.not.deep.include({
type: 'direct',
cid: fixtures.files[1].cid
})
expect(pinset).to.not.deep.include({
type: 'recursive',
cid: fixtures.directory.cid
})
expect(pinset).to.deep.include({
type: 'indirect',
cid: fixtures.directory.files[0].cid
})
expect(pinset).to.deep.include({
type: 'indirect',
cid: fixtures.directory.files[1].cid
})
})
it('should list all types of pins', async () => {
const pinset = await all(ipfs.pin.ls())
expect(pinset).to.not.be.empty()
// check the three "roots"
expect(pinset).to.deep.include({
type: 'recursive',
cid: fixtures.directory.cid
})
expect(pinset).to.deep.include({
type: 'recursive',
cid: fixtures.files[0].cid
})
expect(pinset).to.deep.include({
type: 'direct',
cid: fixtures.files[1].cid
})
expect(pinset).to.deep.include({
type: 'indirect',
cid: fixtures.directory.files[0].cid
})
expect(pinset).to.deep.include({
type: 'indirect',
cid: fixtures.directory.files[1].cid
})
})
it('should list all direct pins', async () => {
const pinset = await all(ipfs.pin.ls({ type: 'direct' }))
expect(pinset).to.have.lengthOf(1)
expect(pinset).to.deep.include({
type: 'direct',
cid: fixtures.files[1].cid
})
})
it('should list pins for a specific hash', async () => {
const pinset = await all(ipfs.pin.ls({
paths: fixtures.files[0].cid
}))
expect(pinset).to.have.lengthOf(1)
expect(pinset).to.have.deep.members([{
type: 'recursive',
cid: fixtures.files[0].cid
}])
})
it('should throw an error on missing direct pins for existing path', () => {
// ipfs.txt is an indirect pin, so lookup for direct one should throw an error
return expect(all(ipfs.pin.ls({
paths: `/ipfs/${fixtures.directory.cid}/files/ipfs.txt`,
type: 'direct'
})))
.to.eventually.be.rejected
.and.be.an.instanceOf(Error)
.and.to.have.property('message', `path '/ipfs/${fixtures.directory.cid}/files/ipfs.txt' is not pinned`)
})
it('should throw an error on missing link for a specific path', () => {
return expect(all(ipfs.pin.ls({
paths: `/ipfs/${fixtures.directory.cid}/I-DONT-EXIST.txt`,
type: 'direct'
})))
.to.eventually.be.rejected
.and.be.an.instanceOf(Error)
.and.to.have.property('message', `no link named "I-DONT-EXIST.txt" under ${fixtures.directory.cid}`)
})
it('should list indirect pins for a specific path', async () => {
const pinset = await all(ipfs.pin.ls({
paths: `/ipfs/${fixtures.directory.cid}/files/ipfs.txt`,
type: 'indirect'
}))
expect(pinset).to.have.lengthOf(1)
expect(pinset).to.deep.include({
type: `indirect through ${fixtures.directory.cid}`,
cid: fixtures.directory.files[1].cid
})
})
it('should list recursive pins for a specific hash', async () => {
const pinset = await all(ipfs.pin.ls({
paths: fixtures.files[0].cid,
type: 'recursive'
}))
expect(pinset).to.have.lengthOf(1)
expect(pinset).to.deep.include({
type: 'recursive',
cid: fixtures.files[0].cid
})
})
it('should list pins for multiple CIDs', async () => {
const pinset = await all(ipfs.pin.ls({
paths: [fixtures.files[0].cid, fixtures.files[1].cid]
}))
const cids = pinset.map(p => p.cid.toString())
expect(cids).to.include(fixtures.files[0].cid.toString())
expect(cids).to.include(fixtures.files[1].cid.toString())
})
it('should throw error for invalid non-string pin type option', () => {
return expect(all(ipfs.pin.ls({ type: 6 })))
.to.eventually.be.rejected()
// TODO: go-ipfs does not return error codes
// .with.property('code').that.equals('ERR_INVALID_PIN_TYPE')
})
it('should throw error for invalid string pin type option', () => {
return expect(all(ipfs.pin.ls({ type: '__proto__' })))
.to.eventually.be.rejected()
// TODO: go-ipfs does not return error codes
// .with.property('code').that.equals('ERR_INVALID_PIN_TYPE')
})
it('should list pins with metadata', async () => {
const { cid } = await ipfs.add(`data-${Math.random()}`, {
pin: false
})
const metadata = {
key: 'value',
one: 2,
array: [{
thing: 'subthing'
}],
obj: {
foo: 'bar',
baz: ['qux']
}
}
await ipfs.pin.add(cid, {
recursive: false,
metadata
})
const pinset = await all(ipfs.pin.ls({
paths: cid
}))
expect(pinset).to.have.deep.members([{
type: 'direct',
cid: cid,
metadata
}])
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/pin/remote/add.js
================================================
/* eslint-env mocha */
import { fixtures, clearRemotePins, clearServices } from '../utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testAdd (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
const ENDPOINT = new URL(process.env.PINNING_SERVICE_ENDPOINT || '')
const KEY = `${process.env.PINNING_SERVICE_KEY}`
const SERVICE = 'pinbot'
describe('.pin.remote.add', function () {
this.timeout(50 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
await ipfs.pin.remote.service.add(SERVICE, {
endpoint: ENDPOINT,
key: KEY
})
})
after(async () => {
await clearServices(ipfs)
await factory.clean()
})
beforeEach(async () => {
await clearRemotePins(ipfs)
})
it('should add a CID and return the added CID', async () => {
const pin = await ipfs.pin.remote.add(fixtures.files[0].cid, {
name: 'fixtures-files-0',
background: true,
service: SERVICE
})
expect(pin).to.deep.equal({
status: 'queued',
cid: fixtures.files[0].cid,
name: 'fixtures-files-0'
})
})
it('should fail if service is not provided', async () => {
const result = ipfs.pin.remote.add(fixtures.files[0].cid, {
name: 'fixtures-files-0',
background: true
})
await expect(result).to.eventually.be.rejectedWith(/service name must be passed/)
})
it('if name is not provided defaults to ""', async () => {
const pin = await ipfs.pin.remote.add(fixtures.files[0].cid, {
background: true,
service: SERVICE
})
expect(pin).to.deep.equal({
cid: fixtures.files[0].cid,
name: '',
status: 'queued'
})
})
it('should default to blocking pin', async () => {
const { cid } = fixtures.files[0]
const result = ipfs.pin.remote.add(cid, {
service: SERVICE
})
const timeout = {}
const winner = await Promise.race([
result,
new Promise(resolve => setTimeout(resolve, 100, timeout))
])
expect(winner).to.equal(timeout)
// trigger status change on the mock service
ipfs.pin.remote.add(cid, {
service: SERVICE,
name: 'pinned-block'
})
expect(await result).to.deep.equal({
cid,
status: 'pinned',
name: ''
})
})
it('should pin dag-cbor', async () => {
const cid = await ipfs.dag.put({}, {
storeCodec: 'dag-cbor',
hashAlg: 'sha2-256'
})
const pin = await ipfs.pin.remote.add(cid, {
service: SERVICE,
name: 'cbor-pin',
background: true
})
expect(pin).to.deep.equal({
cid,
name: 'cbor-pin',
status: 'queued'
})
})
it('should pin raw', async () => {
const cid = await ipfs.dag.put(new Uint8Array(0), {
storeCodec: 'raw',
hashAlg: 'sha2-256'
})
const pin = await ipfs.pin.remote.add(cid, {
service: SERVICE,
background: true
})
expect(pin).to.deep.equal({
cid,
status: 'queued',
name: ''
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/pin/remote/index.js
================================================
import { createSuite } from '../../utils/suite.js'
import { testService } from './service.js'
import { testAdd } from './add.js'
import { testLs } from './ls.js'
import { testRm } from './rm.js'
import { testRmAll } from './rm-all.js'
const tests = {
service: testService,
add: testAdd,
ls: testLs,
rm: testRm,
rmAll: testRmAll
}
export default createSuite(tests, 'pin')
================================================
FILE: packages/interface-ipfs-core/src/pin/remote/ls.js
================================================
/* eslint-env mocha */
import { clearRemotePins, addRemotePins, clearServices } from '../utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../../utils/mocha.js'
import all from 'it-all'
import { CID } from 'multiformats/cid'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testLs (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
const ENDPOINT = new URL(process.env.PINNING_SERVICE_ENDPOINT || '')
const KEY = `${process.env.PINNING_SERVICE_KEY}`
const SERVICE = 'pinbot'
const cid1 = CID.parse('QmbKtKBrmeRHjNCwR4zAfCJdMVu6dgmwk9M9AE9pUM9RgG')
const cid2 = CID.parse('QmdFyxZXsFiP4csgfM5uPu99AvFiKH62CSPDw5TP92nr7w')
const cid3 = CID.parse('Qma4hjFTnCasJ8PVp3mZbZK5g2vGDT4LByLJ7m8ciyRFZP')
const cid4 = CID.parse('QmY9cxiHqTFoWamkQVkpmmqzBrY3hCBEL2XNu3NtX74Fuu')
describe('.pin.remote.ls', function () {
this.timeout(50 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
await ipfs.pin.remote.service.add(SERVICE, {
endpoint: ENDPOINT,
key: KEY
})
})
after(async () => {
await clearServices(ipfs)
await factory.clean()
})
beforeEach(async () => {
await clearRemotePins(ipfs)
})
it('requires service option', async () => {
const result = ipfs.pin.remote.ls({})
await expect(all(result)).to.eventually.be.rejectedWith(/service name must be passed/)
})
it('list no pins', async () => {
const result = ipfs.pin.remote.ls({ service: SERVICE })
const pins = await all(result)
expect(pins).to.deep.equal([])
})
describe('list pins by status', () => {
it('list only pinned pins by default', async () => {
await addRemotePins(ipfs, SERVICE, {
one: cid1,
'pinned-two': cid2,
'pinning-three': cid3,
'failed-four': cid4
})
const list = await all(ipfs.pin.remote.ls({
service: SERVICE
}))
expect(list).to.deep.equal([
{
status: 'pinned',
cid: cid2,
name: 'pinned-two'
}
])
})
it('should list "queued" pins', async () => {
await addRemotePins(ipfs, SERVICE, {
one: cid1,
'pinned-two': cid2,
'pinning-three': cid3,
'failed-four': cid4
})
const list = await all(ipfs.pin.remote.ls({
status: ['queued'],
service: SERVICE
}))
expect(list).to.deep.equal([
{
status: 'queued',
cid: cid1,
name: 'one'
}
])
})
it('should list "pinning" pins', async () => {
await addRemotePins(ipfs, SERVICE, {
one: cid1,
'pinned-two': cid2,
'pinning-three': cid3,
'failed-four': cid4
})
const list = await all(ipfs.pin.remote.ls({
status: ['pinning'],
service: SERVICE
}))
expect(list).to.deep.equal([
{
status: 'pinning',
cid: cid3,
name: 'pinning-three'
}
])
})
it('should list "failed" pins', async () => {
await addRemotePins(ipfs, SERVICE, {
one: cid1,
'pinned-two': cid2,
'pinning-three': cid3,
'failed-four': cid4
})
const list = await all(ipfs.pin.remote.ls({
status: ['failed'],
service: SERVICE
}))
expect(list).to.deep.equal([
{
status: 'failed',
cid: cid4,
name: 'failed-four'
}
])
})
it('should list queued+pinned pins', async () => {
await addRemotePins(ipfs, SERVICE, {
one: cid1,
'pinned-two': cid2,
'pinning-three': cid3,
'failed-four': cid4
})
const list = await all(ipfs.pin.remote.ls({
status: ['queued', 'pinned'],
service: SERVICE
}))
expect(list.sort(byCID)).to.deep.equal([
{
status: 'queued',
cid: cid1,
name: 'one'
},
{
status: 'pinned',
cid: cid2,
name: 'pinned-two'
}
].sort(byCID))
})
it('should list queued+pinned+pinning pins', async () => {
await addRemotePins(ipfs, SERVICE, {
one: cid1,
'pinned-two': cid2,
'pinning-three': cid3,
'failed-four': cid4
})
const list = await all(ipfs.pin.remote.ls({
status: ['queued', 'pinned', 'pinning'],
service: SERVICE
}))
expect(list.sort(byCID)).to.deep.equal([
{
status: 'queued',
cid: cid1,
name: 'one'
},
{
status: 'pinned',
cid: cid2,
name: 'pinned-two'
},
{
status: 'pinning',
cid: cid3,
name: 'pinning-three'
}
].sort(byCID))
})
it('should list queued+pinned+pinning+failed pins', async () => {
await addRemotePins(ipfs, SERVICE, {
one: cid1,
'pinned-two': cid2,
'pinning-three': cid3,
'failed-four': cid4
})
const list = await all(ipfs.pin.remote.ls({
status: ['queued', 'pinned', 'pinning', 'failed'],
service: SERVICE
}))
expect(list.sort(byCID)).to.deep.equal([
{
status: 'queued',
cid: cid1,
name: 'one'
},
{
status: 'pinned',
cid: cid2,
name: 'pinned-two'
},
{
status: 'pinning',
cid: cid3,
name: 'pinning-three'
},
{
status: 'failed',
cid: cid4,
name: 'failed-four'
}
].sort(byCID))
})
})
describe('list pins by name', () => {
it('should list no pins when names do not match', async () => {
await addRemotePins(ipfs, SERVICE, {
a: cid1,
b: cid2,
c: cid3
})
const list = await all(ipfs.pin.remote.ls({
name: 'd',
status: ['queued', 'pinning', 'pinned', 'failed'],
service: SERVICE
}))
expect(list).to.deep.equal([])
})
it('should list only pins with matchin names', async () => {
await addRemotePins(ipfs, SERVICE, {
a: cid1,
b: cid2
})
await addRemotePins(ipfs, SERVICE, {
a: cid3,
b: cid4
})
const list = await all(ipfs.pin.remote.ls({
name: 'a',
status: ['queued', 'pinning', 'pinned', 'failed'],
service: SERVICE
}))
expect(list.sort(byCID)).to.deep.equal([
{
status: 'queued',
name: 'a',
cid: cid1
},
{
status: 'queued',
name: 'a',
cid: cid3
}
].sort(byCID))
})
it('should list only pins with matchin names & status', async () => {
await addRemotePins(ipfs, SERVICE, {
a: cid1,
b: cid2
})
await addRemotePins(ipfs, SERVICE, {
a: cid3,
b: cid4
})
// update status
await addRemotePins(ipfs, SERVICE, {
'pinned-a': cid3
})
const list = await all(ipfs.pin.remote.ls({
name: 'a',
status: ['pinned'],
service: SERVICE
}))
expect(list).to.deep.equal([
{
status: 'pinned',
name: 'a',
cid: cid3
}
])
})
})
describe('list pins by cid', () => {
it('should list pins with matching cid', async () => {
await addRemotePins(ipfs, SERVICE, {
a: cid1,
b: cid2,
c: cid3,
d: cid4
})
const list = await all(ipfs.pin.remote.ls({
cid: [cid1],
status: ['queued', 'pinned', 'pinning', 'failed'],
service: SERVICE
}))
expect(list).to.deep.equal([
{
status: 'queued',
cid: cid1,
name: 'a'
}
])
})
it('should list pins with any matching cid', async () => {
await addRemotePins(ipfs, SERVICE, {
a: cid1,
b: cid2,
c: cid3,
d: cid4
})
const list = await all(ipfs.pin.remote.ls({
cid: [cid1, cid3],
status: ['queued', 'pinned', 'pinning', 'failed'],
service: SERVICE
}))
expect(list.sort(byCID)).to.deep.equal([
{
status: 'queued',
cid: cid1,
name: 'a'
},
{
status: 'queued',
cid: cid3,
name: 'c'
}
].sort(byCID))
})
it('should list pins with matching cid+status', async () => {
await addRemotePins(ipfs, SERVICE, {
'pinned-a': cid1,
'failed-b': cid2,
'pinned-c': cid3,
d: cid4
})
const list = await all(ipfs.pin.remote.ls({
cid: [cid1, cid2],
status: ['pinned', 'failed'],
service: SERVICE
}))
expect(list.sort(byCID)).to.deep.equal([
{
status: 'pinned',
cid: cid1,
name: 'pinned-a'
},
{
status: 'failed',
cid: cid2,
name: 'failed-b'
}
].sort(byCID))
})
it('should list pins with matching cid+status+name', async () => {
await addRemotePins(ipfs, SERVICE, {
'pinned-a': cid1,
'failed-b': cid2,
'pinned-c': cid3,
d: cid4
})
const list = await all(ipfs.pin.remote.ls({
cid: [cid4, cid1, cid2],
name: 'd',
status: ['queued', 'pinned'],
service: SERVICE
}))
expect(list).to.deep.equal([
{
status: 'queued',
cid: cid4,
name: 'd'
}
])
})
})
})
}
/**
* @param {{ cid: CID }} a
* @param {{ cid: CID }} b
*/
const byCID = (a, b) => a.cid.toString() > b.cid.toString() ? 1 : -1
================================================
FILE: packages/interface-ipfs-core/src/pin/remote/rm-all.js
================================================
/* eslint-env mocha */
import { clearRemotePins, addRemotePins, clearServices } from '../utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../../utils/mocha.js'
import { CID } from 'multiformats/cid'
import all from 'it-all'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testRmAll (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
const ENDPOINT = new URL(process.env.PINNING_SERVICE_ENDPOINT || '')
const KEY = `${process.env.PINNING_SERVICE_KEY}`
const SERVICE = 'pinbot'
const cid1 = CID.parse('QmbKtKBrmeRHjNCwR4zAfCJdMVu6dgmwk9M9AE9pUM9RgG')
const cid2 = CID.parse('QmdFyxZXsFiP4csgfM5uPu99AvFiKH62CSPDw5TP92nr7w')
const cid3 = CID.parse('Qma4hjFTnCasJ8PVp3mZbZK5g2vGDT4LByLJ7m8ciyRFZP')
const cid4 = CID.parse('QmY9cxiHqTFoWamkQVkpmmqzBrY3hCBEL2XNu3NtX74Fuu')
describe('.pin.remote.rmAll', function () {
this.timeout(50 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
await ipfs.pin.remote.service.add(SERVICE, {
endpoint: ENDPOINT,
key: KEY
})
})
after(async () => {
await clearServices(ipfs)
await factory.clean()
})
beforeEach(async () => {
await addRemotePins(ipfs, SERVICE, {
'queued-a': cid1,
'pinning-b': cid2,
'pinned-c': cid3,
'failed-d': cid4
})
})
afterEach(async () => {
await clearRemotePins(ipfs)
})
it('.rmAll requires service option', async () => {
const result = ipfs.pin.remote.rmAll({})
await expect(result).to.eventually.be.rejectedWith(/service name must be passed/)
})
it('noop if there is no match', async () => {
await ipfs.pin.remote.rmAll({
cid: [cid1],
status: ['pinned', 'failed'],
service: SERVICE
})
const list = await all(ipfs.pin.remote.ls({
status: ['queued', 'pinning', 'pinned', 'failed'],
service: SERVICE
}))
expect(list.sort(byCID)).to.deep.equal([
{
cid: cid1,
status: 'queued',
name: 'queued-a'
},
{
cid: cid2,
status: 'pinning',
name: 'pinning-b'
},
{
cid: cid3,
status: 'pinned',
name: 'pinned-c'
},
{
cid: cid4,
status: 'failed',
name: 'failed-d'
}
].sort(byCID))
})
it('removes matching pin', async () => {
await ipfs.pin.remote.rmAll({
cid: [cid1],
status: ['queued', 'pinning', 'pinned', 'failed'],
service: SERVICE
})
const list = await all(ipfs.pin.remote.ls({
status: ['queued', 'pinning', 'pinned', 'failed'],
service: SERVICE
}))
expect(list.sort(byCID)).to.deep.equal([
{
cid: cid2,
status: 'pinning',
name: 'pinning-b'
},
{
cid: cid3,
status: 'pinned',
name: 'pinned-c'
},
{
cid: cid4,
status: 'failed',
name: 'failed-d'
}
].sort(byCID))
})
it('removes multiple matches', async () => {
const result = ipfs.pin.remote.rmAll({
cid: [cid1, cid2],
status: ['queued', 'pinning', 'pinned', 'failed'],
service: SERVICE
})
await expect(result).to.eventually.be.equal(undefined)
const list = await all(ipfs.pin.remote.ls({
status: ['queued', 'pinning', 'pinned', 'failed'],
service: SERVICE
}))
expect(list.sort(byCID)).to.deep.equal([
{
cid: cid3,
status: 'pinned',
name: 'pinned-c'
},
{
cid: cid4,
status: 'failed',
name: 'failed-d'
}
].sort(byCID))
})
})
}
/**
* @param {{ cid: CID }} a
* @param {{ cid: CID }} b
*/
const byCID = (a, b) => a.cid.toString() > b.cid.toString() ? 1 : -1
================================================
FILE: packages/interface-ipfs-core/src/pin/remote/rm.js
================================================
/* eslint-env mocha */
import { clearRemotePins, addRemotePins, clearServices } from '../utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../../utils/mocha.js'
import { CID } from 'multiformats/cid'
import all from 'it-all'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testRm (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
const ENDPOINT = new URL(process.env.PINNING_SERVICE_ENDPOINT || '')
const KEY = `${process.env.PINNING_SERVICE_KEY}`
const SERVICE = 'pinbot'
const cid1 = CID.parse('QmbKtKBrmeRHjNCwR4zAfCJdMVu6dgmwk9M9AE9pUM9RgG')
const cid2 = CID.parse('QmdFyxZXsFiP4csgfM5uPu99AvFiKH62CSPDw5TP92nr7w')
const cid3 = CID.parse('Qma4hjFTnCasJ8PVp3mZbZK5g2vGDT4LByLJ7m8ciyRFZP')
const cid4 = CID.parse('QmY9cxiHqTFoWamkQVkpmmqzBrY3hCBEL2XNu3NtX74Fuu')
describe('.pin.remote.rm', function () {
this.timeout(50 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
await ipfs.pin.remote.service.add(SERVICE, {
endpoint: ENDPOINT,
key: KEY
})
})
after(async () => {
await clearServices(ipfs)
await factory.clean()
})
beforeEach(async () => {
await addRemotePins(ipfs, SERVICE, {
'queued-a': cid1,
'pinning-b': cid2,
'pinned-c': cid3,
'failed-d': cid4
})
})
afterEach(async () => {
await clearRemotePins(ipfs)
})
it('.rm requires service option', async () => {
const result = ipfs.pin.remote.rm({})
await expect(result).to.eventually.be.rejectedWith(/service name must be passed/)
})
it('.rmAll requires service option', async () => {
const result = ipfs.pin.remote.rmAll({})
await expect(result).to.eventually.be.rejectedWith(/service name must be passed/)
})
it('noop if there is no match', async () => {
await ipfs.pin.remote.rm({
cid: [cid1],
status: ['pinned', 'failed'],
service: SERVICE
})
const list = await all(ipfs.pin.remote.ls({
status: ['queued', 'pinning', 'pinned', 'failed'],
service: SERVICE
}))
expect(list.sort(byCID)).to.deep.equal([
{
cid: cid1,
status: 'queued',
name: 'queued-a'
},
{
cid: cid2,
status: 'pinning',
name: 'pinning-b'
},
{
cid: cid3,
status: 'pinned',
name: 'pinned-c'
},
{
cid: cid4,
status: 'failed',
name: 'failed-d'
}
].sort(byCID))
})
it('removes matching pin', async () => {
await ipfs.pin.remote.rm({
cid: [cid1],
status: ['queued', 'pinning', 'pinned', 'failed'],
service: SERVICE
})
const list = await all(ipfs.pin.remote.ls({
status: ['queued', 'pinning', 'pinned', 'failed'],
service: SERVICE
}))
expect(list.sort(byCID)).to.deep.equal([
{
cid: cid2,
status: 'pinning',
name: 'pinning-b'
},
{
cid: cid3,
status: 'pinned',
name: 'pinned-c'
},
{
cid: cid4,
status: 'failed',
name: 'failed-d'
}
].sort(byCID))
})
it('fails on multiple matches', async () => {
const result = ipfs.pin.remote.rm({
cid: [cid1, cid2],
status: ['queued', 'pinning', 'pinned', 'failed'],
service: SERVICE
})
await expect(result).to.eventually.be.rejectedWith(
/multiple remote pins are matching this query/
)
const list = await all(ipfs.pin.remote.ls({
status: ['queued', 'pinning', 'pinned', 'failed'],
service: SERVICE
}))
expect(list.sort(byCID)).to.deep.equal([
{
cid: cid1,
status: 'queued',
name: 'queued-a'
},
{
cid: cid2,
status: 'pinning',
name: 'pinning-b'
},
{
cid: cid3,
status: 'pinned',
name: 'pinned-c'
},
{
cid: cid4,
status: 'failed',
name: 'failed-d'
}
].sort(byCID))
})
})
}
/**
* @param {{ cid: CID }} a
* @param {{ cid: CID }} b
*/
const byCID = (a, b) => a.cid.toString() > b.cid.toString() ? 1 : -1
================================================
FILE: packages/interface-ipfs-core/src/pin/remote/service.js
================================================
/* eslint-env mocha */
import { clearServices } from '../utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testService (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
const ENDPOINT = new URL(process.env.PINNING_SERVICE_ENDPOINT || '')
const KEY = `${process.env.PINNING_SERVICE_KEY}`
describe('.pin.remote.service', function () {
this.timeout(50 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(async () => {
await factory.clean()
})
afterEach(() => clearServices(ipfs))
describe('.pin.remote.service.add', () => {
it('should add a service', async () => {
await ipfs.pin.remote.service.add('pinbot', {
endpoint: ENDPOINT,
key: KEY
})
const services = await ipfs.pin.remote.service.ls()
expect(services).to.deep.equal([{
service: 'pinbot',
endpoint: ENDPOINT
}])
})
it('service add requires endpoint', async () => {
// @ts-expect-error missing property
const result = ipfs.pin.remote.service.add('noend', { key: 'token' })
await expect(result).to.eventually.be.rejectedWith(/is required/)
})
it('service add requires key', async () => {
// @ts-expect-error missing property
const result = ipfs.pin.remote.service.add('nokey', {
endpoint: ENDPOINT
})
await expect(result).to.eventually.be.rejectedWith(/is required/)
})
it('add multiple services', async () => {
await ipfs.pin.remote.service.add('pinbot', {
endpoint: ENDPOINT,
key: KEY
})
await ipfs.pin.remote.service.add('pinata', {
endpoint: new URL('https://api.pinata.cloud'),
key: 'somekey'
})
const services = await ipfs.pin.remote.service.ls()
expect(services.sort(byName)).to.deep.equal([
{
service: 'pinbot',
endpoint: ENDPOINT
},
{
service: 'pinata',
endpoint: new URL('https://api.pinata.cloud')
}
].sort(byName))
})
it('can not add service with existing name', async () => {
await ipfs.pin.remote.service.add('pinbot', {
endpoint: ENDPOINT,
key: KEY
})
const result = ipfs.pin.remote.service.add('pinbot', {
endpoint: new URL('http://pinbot.io/'),
key: KEY
})
await expect(result).to.eventually.be.rejectedWith(/service already present/)
})
})
describe('.pin.remote.service.ls', () => {
it('should list services', async () => {
const services = await ipfs.pin.remote.service.ls()
expect(services).to.deep.equal([])
})
it('should list added service', async () => {
await ipfs.pin.remote.service.add('pinbot', {
endpoint: ENDPOINT,
key: KEY
})
const services = await ipfs.pin.remote.service.ls()
expect(services).to.deep.equal([{
service: 'pinbot',
endpoint: ENDPOINT
}])
})
it('should include service stats', async () => {
await ipfs.pin.remote.service.add('pinbot', {
endpoint: ENDPOINT,
key: KEY
})
const services = await ipfs.pin.remote.service.ls({ stat: true })
expect(services).to.deep.equal([{
service: 'pinbot',
endpoint: ENDPOINT,
stat: {
status: 'valid',
pinCount: {
queued: 0,
pinning: 0,
pinned: 0,
failed: 0
}
}
}])
})
it('should report unreachable services', async () => {
await ipfs.pin.remote.service.add('pinbot', {
endpoint: ENDPOINT,
key: KEY
})
await ipfs.pin.remote.service.add('boombot', {
// @ts-expect-error invalid property
endpoint: 'http://127.0.0.1:5555',
key: 'boom'
})
const services = await ipfs.pin.remote.service.ls({ stat: true })
expect(services.sort(byName)).to.deep.equal([
{
service: 'pinbot',
endpoint: ENDPOINT,
stat: {
status: 'valid',
pinCount: {
queued: 0,
pinning: 0,
pinned: 0,
failed: 0
}
}
},
{
service: 'boombot',
endpoint: new URL('http://127.0.0.1:5555'),
stat: {
status: 'invalid'
}
}
].sort(byName))
})
})
describe('.pin.remote.service.rm', () => {
it('should remove service', async () => {
await ipfs.pin.remote.service.add('pinbot', {
endpoint: ENDPOINT,
key: KEY
})
const services = await ipfs.pin.remote.service.ls()
expect(services).to.deep.equal([{
service: 'pinbot',
endpoint: ENDPOINT
}])
await ipfs.pin.remote.service.rm('pinbot')
expect(await ipfs.pin.remote.service.ls()).to.deep.equal([])
})
it('should not fail if service does not registered', async () => {
expect(await ipfs.pin.remote.service.ls()).to.deep.equal([])
expect(await ipfs.pin.remote.service.rm('pinbot')).to.equal(undefined)
})
it('expects service name', async () => {
// @ts-expect-error invalid arg
const result = ipfs.pin.remote.service.rm()
await expect(result).to.eventually.be.rejectedWith(/is required/)
})
})
})
}
/**
* @param {{ service: string }} a
* @param {{ service: string }} b
*/
const byName = (a, b) => a.service > b.service ? 1 : -1
================================================
FILE: packages/interface-ipfs-core/src/pin/rm-all.js
================================================
/* eslint-env mocha */
import { fixtures, clearPins } from './utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import all from 'it-all'
import drain from 'it-drain'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testRmAll (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.pin.rmAll', function () {
this.timeout(50 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
beforeEach(async () => {
ipfs = (await factory.spawn()).api
const dir = fixtures.directory.files.map((file) => ({ path: file.path, content: file.data }))
await all(ipfs.addAll(dir, { pin: false, cidVersion: 0 }))
await ipfs.add(fixtures.files[0].data, { pin: false })
await ipfs.add(fixtures.files[1].data, { pin: false })
})
after(() => factory.clean())
beforeEach(() => {
return clearPins(ipfs)
})
it('should pipe the output of ls to rm', async () => {
await ipfs.pin.add(fixtures.directory.cid)
await drain(ipfs.pin.rmAll(ipfs.pin.ls({ type: 'recursive' })))
await expect(all(ipfs.pin.ls())).to.eventually.have.lengthOf(0)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/pin/rm.js
================================================
/* eslint-env mocha */
import { fixtures, expectPinned, clearPins } from './utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import all from 'it-all'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testRm (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.pin.rm', function () {
this.timeout(50 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
beforeEach(async () => {
ipfs = (await factory.spawn()).api
const dir = fixtures.directory.files.map((file) => ({ path: file.path, content: file.data }))
await all(ipfs.addAll(dir, { pin: false, cidVersion: 0 }))
await ipfs.add(fixtures.files[0].data, { pin: false })
await ipfs.add(fixtures.files[1].data, { pin: false })
})
after(() => factory.clean())
beforeEach(() => {
return clearPins(ipfs)
})
it('should remove a recursive pin', async () => {
await ipfs.pin.add(fixtures.directory.cid)
const unpinnedCid = await ipfs.pin.rm(fixtures.directory.cid, { recursive: true })
expect(unpinnedCid).to.deep.equal(fixtures.directory.cid)
const pinset = await all(ipfs.pin.ls({ type: 'recursive' }))
expect(pinset).to.not.deep.include({
cid: fixtures.directory.cid,
type: 'recursive'
})
})
it('should remove a direct pin', async () => {
await ipfs.pin.add(fixtures.directory.cid, { recursive: false })
const unpinnedCid = await ipfs.pin.rm(fixtures.directory.cid, { recursive: false })
expect(unpinnedCid).to.deep.equal(fixtures.directory.cid)
const pinset = await all(ipfs.pin.ls({ type: 'direct' }))
expect(pinset.map(p => p.cid)).to.not.deep.include(fixtures.directory.cid)
})
it('should fail to remove an indirect pin', async () => {
await ipfs.pin.add(fixtures.directory.cid, {
recursive: true
})
await expect(ipfs.pin.rm(fixtures.directory.files[0].cid))
.to.eventually.be.rejectedWith(/pinned indirectly/)
await expectPinned(ipfs, fixtures.directory.files[0].cid)
})
it('should fail when an item is not pinned', async () => {
await expect(ipfs.pin.rm(fixtures.directory.cid))
.to.eventually.be.rejectedWith(/not pinned/)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/pin/utils.js
================================================
import { expect } from 'aegir/chai'
import loadFixture from 'aegir/fixtures'
import { CID } from 'multiformats/cid'
import drain from 'it-drain'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import first from 'it-first'
export const pinTypes = {
direct: 'direct',
recursive: 'recursive',
indirect: 'indirect',
all: 'all'
}
export const fixtures = Object.freeze({
// NOTE: files under 'directory' need to be different than standalone ones in 'files'
directory: Object.freeze({
cid: CID.parse('QmY8KdYQSYKFU5hM7F5ioZ5yYSgV5VZ1kDEdqfRL3rFgcd'),
files: Object.freeze([Object.freeze({
path: 'test-folder/ipfs-add.js',
data: loadFixture('test/fixtures/test-folder/ipfs-add.js', 'interface-ipfs-core'),
cid: CID.parse('QmbKtKBrmeRHjNCwR4zAfCJdMVu6dgmwk9M9AE9pUM9RgG')
}), Object.freeze({
path: 'test-folder/files/ipfs.txt',
data: loadFixture('test/fixtures/test-folder/files/ipfs.txt', 'interface-ipfs-core'),
cid: CID.parse('QmdFyxZXsFiP4csgfM5uPu99AvFiKH62CSPDw5TP92nr7w')
})])
}),
files: Object.freeze([Object.freeze({
data: uint8ArrayFromString('Plz add me!\n'),
cid: CID.parse('Qma4hjFTnCasJ8PVp3mZbZK5g2vGDT4LByLJ7m8ciyRFZP')
}), Object.freeze({
data: loadFixture('test/fixtures/test-folder/files/hello.txt', 'interface-ipfs-core'),
cid: CID.parse('QmY9cxiHqTFoWamkQVkpmmqzBrY3hCBEL2XNu3NtX74Fuu')
})])
})
/**
* @param {import('ipfs-core-types').IPFS} ipfs
*/
export const clearPins = async (ipfs) => {
await drain(ipfs.pin.rmAll(ipfs.pin.ls({ type: pinTypes.recursive })))
await drain(ipfs.pin.rmAll(ipfs.pin.ls({ type: pinTypes.direct })))
}
/**
* @param {import('ipfs-core-types').IPFS} ipfs
*/
export const clearRemotePins = async (ipfs) => {
for (const { service } of await ipfs.pin.remote.service.ls()) {
const cids = []
const status = ['queued', 'pinning', 'pinned', 'failed']
for await (const pin of ipfs.pin.remote.ls({ status, service })) {
cids.push(pin.cid)
}
if (cids.length > 0) {
await ipfs.pin.remote.rmAll({
cid: cids,
status,
service
})
}
}
}
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {string} service
* @param {Record} pins
*/
export const addRemotePins = async (ipfs, service, pins) => {
const requests = []
for (const [name, cid] of Object.entries(pins)) {
requests.push(ipfs.pin.remote.add(cid, {
name,
service,
background: true
}))
}
await Promise.all(requests)
}
/**
* @param {import('ipfs-core-types').IPFS} ipfs
*/
export const clearServices = async (ipfs) => {
const services = await ipfs.pin.remote.service.ls()
await Promise.all(services.map(({ service }) => ipfs.pin.remote.service.rm(service)))
}
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {CID} cid
* @param {string} type
* @param {boolean} pinned
*/
export const expectPinned = async (ipfs, cid, type = pinTypes.all, pinned = true) => {
if (typeof type === 'boolean') {
pinned = type
type = pinTypes.all
}
const result = await isPinnedWithType(ipfs, cid, type)
expect(result).to.eql(pinned)
}
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {CID} cid
* @param {string} type
*/
export const expectNotPinned = (ipfs, cid, type = pinTypes.all) => {
return expectPinned(ipfs, cid, type, false)
}
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {CID} cid
* @param {string} type
*/
export async function isPinnedWithType (ipfs, cid, type) {
try {
const res = await first(ipfs.pin.ls({ paths: cid, type }))
return Boolean(res)
} catch (/** @type {any} */ err) {
return false
}
}
================================================
FILE: packages/interface-ipfs-core/src/ping/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testPing } from './ping.js'
const tests = {
ping: testPing
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/ping/ping.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import all from 'it-all'
import { isWebWorker } from 'ipfs-utils/src/env.js'
import { peerIdFromString } from '@libp2p/peer-id'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testPing (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.ping', function () {
this.timeout(60 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfsA
/** @type {import('ipfs-core-types').IPFS} */
let ipfsB
/** @type {import('ipfs-core-types/src/root').IDResult} */
let nodeBId
before(async () => {
ipfsA = (await factory.spawn({ type: 'proc' })).api
// webworkers are not dialable because webrtc is not available
ipfsB = (await factory.spawn({ type: isWebWorker ? 'go' : undefined })).api
nodeBId = await ipfsB.id()
await ipfsA.swarm.connect(nodeBId.addresses[0])
})
after(() => factory.clean())
it('should send the specified number of packets', async () => {
const count = 3
const responses = await all(ipfsA.ping(nodeBId.id, { count }))
expect(responses.length).to.be.ok()
expect(responses[0].success).to.be.true()
})
it('should fail when pinging a peer that is not available', () => {
const notAvailablePeerId = peerIdFromString('QmUmaEnH1uMmvckMZbh3yShaasvELPW4ZLPWnB4entMTEn')
const count = 2
return expect(all(ipfsA.ping(notAvailablePeerId, { count }))).to.eventually.be.rejected()
})
it('can ping without options', async () => {
const res = await all(ipfsA.ping(nodeBId.id))
expect(res.length).to.be.ok()
expect(res[0].success).to.be.true()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/ping/utils.js
================================================
import { expect } from 'aegir/chai'
/**
* @param {*} obj
*/
export function expectIsPingResponse (obj) {
expect(obj).to.have.a.property('success')
expect(obj).to.have.a.property('time')
expect(obj).to.have.a.property('text')
expect(obj.success).to.be.a('boolean')
expect(obj.time).to.be.a('number')
expect(obj.text).to.be.a('string')
}
/**
* Determine if a ping response object is a pong, or something else, like a status message
*
* @param {*} pingResponse
*/
export function isPong (pingResponse) {
return Boolean(pingResponse && pingResponse.success && !pingResponse.text)
}
================================================
FILE: packages/interface-ipfs-core/src/pubsub/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testPublish } from './publish.js'
import { testSubscribe } from './subscribe.js'
import { testUnsubscribe } from './unsubscribe.js'
import { testPeers } from './peers.js'
import { testLs } from './ls.js'
const tests = {
publish: testPublish,
subscribe: testSubscribe,
unsubscribe: testUnsubscribe,
peers: testPeers,
ls: testLs
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/pubsub/ls.js
================================================
/* eslint-env mocha */
import { getTopic } from './utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import delay from 'delay'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testLs (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.pubsub.ls', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/** @type {string[]} */
let subscribedTopics = []
before(async () => {
ipfs = (await factory.spawn()).api
})
afterEach(async () => {
for (let i = 0; i < subscribedTopics.length; i++) {
await ipfs.pubsub.unsubscribe(subscribedTopics[i])
}
subscribedTopics = []
await delay(100)
})
after(() => factory.clean())
it('should return an empty list when no topics are subscribed', async () => {
const topics = await ipfs.pubsub.ls()
expect(topics.length).to.equal(0)
})
it('should return a list with 1 subscribed topic', async () => {
const sub1 = () => {}
const topic = getTopic()
subscribedTopics = [topic]
await ipfs.pubsub.subscribe(topic, sub1)
const topics = await ipfs.pubsub.ls()
expect(topics).to.be.eql([topic])
})
it('should return a list with 3 subscribed topics', async () => {
const topics = [{
name: 'one',
handler () {}
}, {
name: 'two',
handler () {}
}, {
name: 'three',
handler () {}
}]
subscribedTopics = topics.map(t => t.name)
for (let i = 0; i < topics.length; i++) {
await ipfs.pubsub.subscribe(topics[i].name, topics[i].handler)
}
const list = await ipfs.pubsub.ls()
expect(list.sort()).to.eql(topics.map(t => t.name).sort())
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/pubsub/peers.js
================================================
/* eslint-env mocha */
import { waitForPeers, getTopic } from './utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import delay from 'delay'
import { isWebWorker } from 'ipfs-utils/src/env.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testPeers (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.pubsub.peers', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs1
/** @type {import('ipfs-core-types').IPFS} */
let ipfs2
/** @type {import('ipfs-core-types').IPFS} */
let ipfs3
/** @type {string[]} */
let subscribedTopics = []
/** @type {import('ipfs-core-types/src/root').IDResult} */
let ipfs2Id
/** @type {import('ipfs-core-types/src/root').IDResult} */
let ipfs3Id
before(async () => {
ipfs1 = (await factory.spawn()).api
// webworkers are not dialable because webrtc is not available
ipfs2 = (await factory.spawn({ type: isWebWorker ? 'js' : undefined })).api
ipfs3 = (await factory.spawn({ type: isWebWorker ? 'js' : undefined })).api
ipfs2Id = await ipfs2.id()
ipfs3Id = await ipfs3.id()
const ipfs2Addr = ipfs2Id.addresses
.find(ma => ma.nodeAddress().address === '127.0.0.1')
const ipfs3Addr = ipfs3Id.addresses
.find(ma => ma.nodeAddress().address === '127.0.0.1')
if (!ipfs2Addr || !ipfs3Addr) {
throw new Error('Could not find addrs')
}
await ipfs1.swarm.connect(ipfs2Addr)
await ipfs1.swarm.connect(ipfs3Addr)
await ipfs2.swarm.connect(ipfs3Addr)
})
afterEach(async () => {
const nodes = [ipfs1, ipfs2, ipfs3]
for (let i = 0; i < subscribedTopics.length; i++) {
const topic = subscribedTopics[i]
await Promise.all(nodes.map(ipfs => ipfs.pubsub.unsubscribe(topic)))
}
subscribedTopics = []
await delay(100)
})
after(() => factory.clean())
it('should not error when not subscribed to a topic', async () => {
const topic = getTopic()
const peers = await ipfs1.pubsub.peers(topic)
expect(peers).to.exist()
// Should be empty() but as mentioned below go-ipfs returns more than it should
// expect(peers).to.be.empty()
})
it('should not return extra peers', async () => {
// Currently go-ipfs returns peers that have not been
// subscribed to the topic. Enable when go-ipfs has been fixed
const sub1 = () => {}
const sub2 = () => {}
const sub3 = () => {}
const topic = getTopic()
const topicOther = topic + 'different topic'
subscribedTopics = [topic, topicOther]
await ipfs1.pubsub.subscribe(topic, sub1)
await ipfs2.pubsub.subscribe(topicOther, sub2)
await ipfs3.pubsub.subscribe(topicOther, sub3)
const peers = await ipfs1.pubsub.peers(topic)
expect(peers).to.be.empty()
})
it('should return peers for a topic - one peer', async () => {
// Currently go-ipfs returns peers that have not been
// subscribed to the topic. Enable when go-ipfs has been fixed
const sub1 = () => {}
const sub2 = () => {}
const sub3 = () => {}
const topic = getTopic()
subscribedTopics = [topic]
await ipfs1.pubsub.subscribe(topic, sub1)
await ipfs2.pubsub.subscribe(topic, sub2)
await ipfs3.pubsub.subscribe(topic, sub3)
await waitForPeers(ipfs1, topic, [ipfs2Id.id], 30000)
})
it('should return peers for a topic - multiple peers', async () => {
const sub1 = () => {}
const sub2 = () => {}
const sub3 = () => {}
const topic = getTopic()
subscribedTopics = [topic]
await ipfs1.pubsub.subscribe(topic, sub1)
await ipfs2.pubsub.subscribe(topic, sub2)
await ipfs3.pubsub.subscribe(topic, sub3)
await waitForPeers(ipfs1, topic, [ipfs2Id.id, ipfs3Id.id], 30000)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/pubsub/publish.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { nanoid } from 'nanoid'
import { getTopic } from './utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import pWaitFor from 'p-wait-for'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
* @typedef {import('ipfs-core-types').IPFS} IPFS
*/
/**
* @param {string} topic
* @param {IPFS} ipfs
* @param {IPFS} remote
*/
async function waitForRemoteToBeSubscribed (topic, ipfs, remote) {
await remote.pubsub.subscribe(topic, () => {})
const remoteId = await remote.id()
// wait for remote to be subscribed to topic
await pWaitFor(async () => {
const peers = await ipfs.pubsub.peers(topic)
return peers.map(p => p.toString()).includes(remoteId.id.toString())
})
}
/**
* @param {Factory} factory
* @param {object} options
*/
export function testPublish (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.pubsub.publish', function () {
this.timeout(80 * 1000)
/** @type {IPFS} */
let ipfs
/** @type {IPFS} */
let remote
before(async () => {
ipfs = (await factory.spawn()).api
remote = (await factory.spawn()).api
// ensure we have peers to allow publishing
const remoteId = await remote.id()
await ipfs.swarm.connect(remoteId.addresses[0])
})
after(() => factory.clean())
it('should fail with undefined msg', async () => {
const topic = getTopic()
await waitForRemoteToBeSubscribed(topic, ipfs, remote)
// @ts-expect-error invalid parameter
await expect(ipfs.pubsub.publish(topic)).to.eventually.be.rejected()
})
it('should publish message from buffer', async () => {
const topic = getTopic()
await waitForRemoteToBeSubscribed(topic, ipfs, remote)
return ipfs.pubsub.publish(topic, uint8ArrayFromString(nanoid()))
})
it('should publish 10 times within time limit', async () => {
const count = 10
const topic = getTopic()
await waitForRemoteToBeSubscribed(topic, ipfs, remote)
for (let i = 0; i < count; i++) {
await ipfs.pubsub.publish(topic, uint8ArrayFromString(nanoid()))
}
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/pubsub/subscribe.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { nanoid } from 'nanoid'
import { pushable } from 'it-pushable'
import all from 'it-all'
import { waitForPeers, getTopic } from './utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import delay from 'delay'
import { isWebWorker, isNode } from 'ipfs-utils/src/env.js'
import sinon from 'sinon'
import defer from 'p-defer'
import pWaitFor from 'p-wait-for'
import { isPeerId } from '@libp2p/interface-peer-id'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
* @typedef {import('@libp2p/interface-pubsub').Message} Message
* @typedef {import('it-pushable').Pushable} Pushable
* @typedef {import('p-defer').DeferredPromise} DeferredMessagePromise
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testSubscribe (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.pubsub.subscribe', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs1
/** @type {import('ipfs-core-types').IPFS} */
let ipfs2
/** @type {string} */
let topic
/** @type {string[]} */
let subscribedTopics = []
/** @type {import('ipfs-core-types/src/root').IDResult} */
let ipfs1Id
/** @type {import('ipfs-core-types/src/root').IDResult} */
let ipfs2Id
beforeEach(async () => {
ipfs1 = (await factory.spawn()).api
// webworkers are not dialable because webrtc is not available
ipfs2 = (await factory.spawn({ type: isWebWorker ? 'js' : undefined })).api
ipfs1Id = await ipfs1.id()
ipfs2Id = await ipfs2.id()
topic = getTopic()
subscribedTopics = [topic]
})
afterEach(async () => {
const nodes = [ipfs1, ipfs2]
for (let i = 0; i < subscribedTopics.length; i++) {
const topic = subscribedTopics[i]
await Promise.all(nodes.map(ipfs => ipfs.pubsub.unsubscribe(topic)))
}
subscribedTopics = []
await delay(100)
await factory.clean()
})
describe('single node', () => {
it('should subscribe to one topic', async () => {
/** @type {import('p-defer').DeferredPromise} */
const deferred = defer()
await ipfs1.pubsub.subscribe(topic, msg => {
deferred.resolve(msg)
})
await ipfs1.pubsub.publish(topic, uint8ArrayFromString('hi'))
const msg = await deferred.promise
if (msg.type !== 'signed') {
throw new Error('Message was not signed')
}
expect(uint8ArrayToString(msg.data)).to.equal('hi')
expect(msg).to.have.property('sequenceNumber')
expect(msg.sequenceNumber).to.be.a('BigInt')
expect(msg.topic).to.eq(topic)
expect(isPeerId(msg.from)).to.be.true()
expect(msg.from.toString()).to.equal(ipfs1Id.id.toString())
})
it('should subscribe to one topic with options', async () => {
const msgStream = pushable({ objectMode: true })
await ipfs1.pubsub.subscribe(topic, msg => {
msgStream.push(msg)
msgStream.end()
}, {})
await ipfs1.pubsub.publish(topic, uint8ArrayFromString('hi'))
for await (const msg of msgStream) {
expect(uint8ArrayToString(msg.data)).to.equal('hi')
expect(msg).to.have.property('sequenceNumber')
expect(msg.sequenceNumber).to.be.a('bigint')
expect(msg.topic).to.eq(topic)
expect(msg.from.toString()).to.equal(ipfs1Id.id.toString())
}
})
it('should subscribe to topic multiple times with different handlers', async () => {
/** @type {import('p-defer').DeferredPromise} */
const msgStream1 = defer()
/** @type {import('p-defer').DeferredPromise} */
const msgStream2 = defer()
/** @type {import('@libp2p/interfaces/events').EventHandler} */
const handler1 = msg => {
msgStream1.resolve(msg)
}
/** @type {import('@libp2p/interfaces/events').EventHandler} */
const handler2 = msg => {
msgStream2.resolve(msg)
}
await Promise.all([
ipfs1.pubsub.subscribe(topic, handler1),
ipfs1.pubsub.subscribe(topic, handler2)
])
await ipfs1.pubsub.publish(topic, uint8ArrayFromString('hello'))
const handler1Msg = await msgStream1.promise
expect(uint8ArrayToString(handler1Msg.data)).to.eql('hello')
const handler2Msg = await msgStream2.promise
expect(uint8ArrayToString(handler2Msg.data)).to.eql('hello')
await ipfs1.pubsub.unsubscribe(topic, handler1)
await delay(100)
// Still subscribed as there is one listener left
expect(await ipfs1.pubsub.ls()).to.eql([topic])
await ipfs1.pubsub.unsubscribe(topic, handler2)
await delay(100)
// Now all listeners are gone no subscription anymore
expect(await ipfs1.pubsub.ls()).to.eql([])
})
it('should allow discover option to be passed', async () => {
const msgStream = pushable({ objectMode: true })
await ipfs1.pubsub.subscribe(topic, msg => {
msgStream.push(msg)
msgStream.end()
}, { discover: true })
await ipfs1.pubsub.publish(topic, uint8ArrayFromString('hi'))
for await (const msg of msgStream) {
expect(uint8ArrayToString(msg.data)).to.eql('hi')
}
})
})
describe('multiple connected nodes', () => {
beforeEach(() => {
if (ipfs1.pubsub.setMaxListeners) {
ipfs1.pubsub.setMaxListeners(100)
}
if (ipfs2.pubsub.setMaxListeners) {
ipfs2.pubsub.setMaxListeners(100)
}
const ipfs2Addr = ipfs2Id.addresses
.find(ma => ma.nodeAddress().address === '127.0.0.1')
if (!ipfs2Addr) {
throw new Error('No address found')
}
return ipfs1.swarm.connect(ipfs2Addr)
})
it('should receive messages from a different node with floodsub', async function () {
if (!isNode) {
return this.skip()
}
const expectedString = 'should receive messages from a different node with floodsub'
const topic = `floodsub-${nanoid()}`
const ipfs1 = (await factory.spawn({
test: true,
ipfsOptions: {
config: {
Pubsub: {
Router: 'floodsub'
}
}
}
})).api
const ipfs1Id = await ipfs1.id()
const ipfs2 = (await factory.spawn({
type: isWebWorker ? 'go' : undefined,
test: true,
ipfsOptions: {
config: {
Pubsub: {
Router: 'floodsub'
}
}
}
})).api
const ipfs2Id = await ipfs2.id()
await ipfs1.swarm.connect(ipfs2Id.addresses[0])
/** @type {DeferredMessagePromise} */
const msgStream1 = defer()
/** @type {DeferredMessagePromise} */
const msgStream2 = defer()
/** @type {import('@libp2p/interfaces/events').EventHandler} */
const sub1 = msg => {
msgStream1.resolve(msg)
}
/** @type {import('@libp2p/interfaces/events').EventHandler} */
const sub2 = msg => {
msgStream2.resolve(msg)
}
await Promise.all([
ipfs1.pubsub.subscribe(topic, sub1),
ipfs2.pubsub.subscribe(topic, sub2)
])
await Promise.all([
waitForPeers(ipfs2, topic, [ipfs1Id.id], 30000),
waitForPeers(ipfs1, topic, [ipfs2Id.id], 30000)
])
await ipfs2.pubsub.publish(topic, uint8ArrayFromString(expectedString))
const sub1Msg = await msgStream1.promise
if (sub1Msg.type !== 'signed') {
throw new Error('Message was not signed')
}
expect(uint8ArrayToString(sub1Msg.data)).to.be.eql(expectedString)
expect(sub1Msg.from.toString()).to.eql(ipfs2Id.id.toString())
const sub2Msg = await msgStream2.promise
if (sub2Msg.type !== 'signed') {
throw new Error('Message was not signed')
}
expect(uint8ArrayToString(sub2Msg.data)).to.be.eql(expectedString)
expect(sub2Msg.from.toString()).to.eql(ipfs2Id.id.toString())
})
it('should receive messages from a different node', async () => {
const expectedString = 'hello from the other side'
/** @type {DeferredMessagePromise} */
const msgStream1 = defer()
/** @type {DeferredMessagePromise} */
const msgStream2 = defer()
/** @type {import('@libp2p/interfaces/events').EventHandler} */
const sub1 = msg => {
msgStream1.resolve(msg)
}
/** @type {import('@libp2p/interfaces/events').EventHandler} */
const sub2 = msg => {
msgStream2.resolve(msg)
}
await Promise.all([
ipfs1.pubsub.subscribe(topic, sub1),
ipfs2.pubsub.subscribe(topic, sub2)
])
await waitForPeers(ipfs2, topic, [ipfs1Id.id], 30000)
await delay(5000) // gossipsub need this delay https://github.com/libp2p/go-libp2p-pubsub/issues/331
await ipfs2.pubsub.publish(topic, uint8ArrayFromString(expectedString))
const sub1Msg = await msgStream1.promise
if (sub1Msg.type !== 'signed') {
throw new Error('Message was not signed')
}
expect(uint8ArrayToString(sub1Msg.data)).to.be.eql(expectedString)
expect(sub1Msg.from.toString()).to.eql(ipfs2Id.id.toString())
const sub2Msg = await msgStream2.promise
if (sub2Msg.type !== 'signed') {
throw new Error('Message was not signed')
}
expect(uint8ArrayToString(sub2Msg.data)).to.be.eql(expectedString)
expect(sub2Msg.from.toString()).to.eql(ipfs2Id.id.toString())
})
it('should round trip a non-utf8 binary buffer', async () => {
const expectedHex = 'a36161636179656162830103056164a16466666666f4'
const buffer = uint8ArrayFromString(expectedHex, 'base16')
/** @type {DeferredMessagePromise} */
const msgStream1 = defer()
/** @type {DeferredMessagePromise} */
const msgStream2 = defer()
/** @type {import('@libp2p/interfaces/events').EventHandler} */
const sub1 = msg => {
msgStream1.resolve(msg)
}
/** @type {import('@libp2p/interfaces/events').EventHandler} */
const sub2 = msg => {
msgStream2.resolve(msg)
}
await Promise.all([
ipfs1.pubsub.subscribe(topic, sub1),
ipfs2.pubsub.subscribe(topic, sub2)
])
await waitForPeers(ipfs2, topic, [ipfs1Id.id], 30000)
await delay(5000) // gossipsub need this delay https://github.com/libp2p/go-libp2p-pubsub/issues/331
await ipfs2.pubsub.publish(topic, buffer)
const sub1Msg = await msgStream1.promise
if (sub1Msg.type !== 'signed') {
throw new Error('Message was not signed')
}
expect(uint8ArrayToString(sub1Msg.data, 'base16')).to.be.eql(expectedHex)
expect(sub1Msg.from.toString()).to.eql(ipfs2Id.id.toString())
const sub2Msg = await msgStream2.promise
if (sub2Msg.type !== 'signed') {
throw new Error('Message was not signed')
}
expect(uint8ArrayToString(sub2Msg.data, 'base16')).to.be.eql(expectedHex)
expect(sub2Msg.from.toString()).to.eql(ipfs2Id.id.toString())
})
it('should receive multiple messages', async () => {
const outbox = ['hello', 'world', 'this', 'is', 'pubsub']
const msgStream1 = pushable({ objectMode: true })
const msgStream2 = pushable({ objectMode: true })
let sub1Called = 0
/** @type {import('@libp2p/interfaces/events').EventHandler} */
const sub1 = msg => {
msgStream1.push(msg)
sub1Called++
if (sub1Called === outbox.length) msgStream1.end()
}
let sub2Called = 0
/** @type {import('@libp2p/interfaces/events').EventHandler} */
const sub2 = msg => {
msgStream2.push(msg)
sub2Called++
if (sub2Called === outbox.length) msgStream2.end()
}
await Promise.all([
ipfs1.pubsub.subscribe(topic, sub1),
ipfs2.pubsub.subscribe(topic, sub2)
])
await waitForPeers(ipfs2, topic, [ipfs1Id.id], 30000)
await delay(5000) // gossipsub need this delay https://github.com/libp2p/go-libp2p-pubsub/issues/331
for (let i = 0; i < outbox.length; i++) {
await ipfs2.pubsub.publish(topic, uint8ArrayFromString(outbox[i]))
}
const sub1Msgs = await all(msgStream1)
sub1Msgs.forEach(msg => expect(msg.from.toString()).to.eql(ipfs2Id.id.toString()))
const inbox1 = sub1Msgs.map(msg => uint8ArrayToString(msg.data))
expect(inbox1.sort()).to.eql(outbox.sort())
const sub2Msgs = await all(msgStream2)
sub2Msgs.forEach(msg => expect(msg.from.toString()).to.eql(ipfs2Id.id.toString()))
const inbox2 = sub2Msgs.map(msg => uint8ArrayToString(msg.data))
expect(inbox2.sort()).to.eql(outbox.sort())
})
it.skip('should send/receive 100 messages', async function () {
this.timeout(2 * 60 * 1000)
const msgBase = 'msg - '
const count = 100
const msgStream = pushable({ objectMode: true })
let subCalled = 0
/** @type {import('@libp2p/interfaces/events').EventHandler} */
const sub = msg => {
msgStream.push(msg)
subCalled++
if (subCalled === count) msgStream.end()
}
await Promise.all([
ipfs1.pubsub.subscribe(topic, sub),
ipfs2.pubsub.subscribe(topic, () => {})
])
await waitForPeers(ipfs1, topic, [ipfs2Id.id], 30000)
await delay(5000) // gossipsub need this delay https://github.com/libp2p/go-libp2p-pubsub/issues/331
const startTime = new Date().getTime()
for (let i = 0; i < count; i++) {
const msgData = uint8ArrayFromString(msgBase + i)
await ipfs2.pubsub.publish(topic, msgData)
}
const msgs = await all(msgStream)
const duration = new Date().getTime() - startTime
const opsPerSec = Math.floor(count / (duration / 1000))
// eslint-disable-next-line
console.log(`Send/Receive 100 messages took: ${duration} ms, ${opsPerSec} ops / s`)
msgs.forEach(msg => {
expect(msg.from.toString()).to.eql(ipfs2Id.id.toString())
expect(uint8ArrayToString(msg.data).startsWith(msgBase)).to.be.true()
})
})
it('should receive messages from a different node on lots of topics', async () => {
this.timeout(5 * 60 * 1000)
const numTopics = 20
const topics = []
const expectedStrings = []
const msgStreams = []
for (let i = 0; i < numTopics; i++) {
const topic = `pubsub-topic-${Math.random()}`
topics.push(topic)
const msgStream1 = pushable({ objectMode: true })
const msgStream2 = pushable({ objectMode: true })
msgStreams.push({
msgStream1,
msgStream2
})
/** @type {import('@libp2p/interfaces/events').EventHandler} */
const sub1 = msg => {
msgStream1.push(msg)
msgStream1.end()
}
/** @type {import('@libp2p/interfaces/events').EventHandler} */
const sub2 = msg => {
msgStream2.push(msg)
msgStream2.end()
}
await Promise.all([
ipfs1.pubsub.subscribe(topic, sub1),
ipfs2.pubsub.subscribe(topic, sub2)
])
await waitForPeers(ipfs2, topic, [ipfs1Id.id], 30000)
}
await delay(5000) // gossipsub needs this delay https://github.com/libp2p/go-libp2p-pubsub/issues/331
for (let i = 0; i < numTopics; i++) {
const expectedString = `hello pubsub ${Math.random()}`
expectedStrings.push(expectedString)
await ipfs2.pubsub.publish(topics[i], uint8ArrayFromString(expectedString))
}
for (let i = 0; i < numTopics; i++) {
const [sub1Msg] = await all(msgStreams[i].msgStream1)
expect(uint8ArrayToString(sub1Msg.data)).to.equal(expectedStrings[i])
expect(sub1Msg.from.toString()).to.eql(ipfs2Id.id.toString())
const [sub2Msg] = await all(msgStreams[i].msgStream2)
expect(uint8ArrayToString(sub2Msg.data)).to.equal(expectedStrings[i])
expect(sub2Msg.from.toString()).to.eql(ipfs2Id.id.toString())
}
})
it('should unsubscribe multiple handlers', async () => {
this.timeout(2 * 60 * 1000)
const topic = `topic-${Math.random()}`
const handler1 = sinon.stub()
const handler2 = sinon.stub()
await Promise.all([
ipfs1.pubsub.subscribe(topic, sinon.stub()),
ipfs2.pubsub.subscribe(topic, handler1),
ipfs2.pubsub.subscribe(topic, handler2)
])
await waitForPeers(ipfs1, topic, [ipfs2Id.id], 30000)
expect(handler1).to.have.property('callCount', 0)
expect(handler2).to.have.property('callCount', 0)
// await gossipsub heartbeat to rebalance mesh
await delay(2000)
await ipfs1.pubsub.publish(topic, uint8ArrayFromString('hello world 1'))
// should receive message
await pWaitFor(() => {
return handler1.callCount === 1 && handler2.callCount === 1
})
// both handlers should be removed
await ipfs2.pubsub.unsubscribe(topic)
await ipfs1.pubsub.publish(topic, uint8ArrayFromString('hello world 2'))
await delay(1000)
// should not have received message
expect(handler1).to.have.property('callCount', 1)
expect(handler2).to.have.property('callCount', 1)
})
it('should unsubscribe individual handlers', async () => {
this.timeout(2 * 60 * 1000)
const topic = `topic-${Math.random()}`
const handler1 = sinon.stub()
const handler2 = sinon.stub()
await Promise.all([
ipfs1.pubsub.subscribe(topic, sinon.stub()),
ipfs2.pubsub.subscribe(topic, handler1),
ipfs2.pubsub.subscribe(topic, handler2)
])
await waitForPeers(ipfs1, topic, [ipfs2Id.id], 30000)
expect(handler1).to.have.property('callCount', 0)
expect(handler2).to.have.property('callCount', 0)
// await gossipsub heartbeat to rebalance mesh
await delay(2000)
await ipfs1.pubsub.publish(topic, uint8ArrayFromString('hello world 1'))
// should receive message
await pWaitFor(() => {
return handler1.callCount === 1 && handler2.callCount === 1
})
// only one handler should be removed
await ipfs2.pubsub.unsubscribe(topic, handler1)
await ipfs1.pubsub.publish(topic, uint8ArrayFromString('hello world 2'))
await delay(1000)
// one should receive message
await pWaitFor(() => {
return handler2.callCount === 2
})
// other should not have received message
expect(handler1).to.have.property('callCount', 1)
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/pubsub/unsubscribe.js
================================================
/* eslint-env mocha */
import { isBrowser, isWebWorker, isElectronRenderer } from 'ipfs-utils/src/env.js'
import { getTopic } from './utils.js'
import { getDescribe, getIt } from '../utils/mocha.js'
import waitFor from '../utils/wait-for.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
* @typedef {import('@libp2p/interface-pubsub').Message} Message
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testUnsubscribe (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.pubsub.unsubscribe', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
// Browser/worker has max ~5 open HTTP requests to the same origin
const count = isBrowser || isWebWorker || isElectronRenderer ? 5 : 10
it(`should subscribe and unsubscribe ${count} times`, async () => {
const someTopic = getTopic()
/** @type {import('@libp2p/interfaces/events').EventHandler[]} */
const handlers = Array.from(Array(count), () => msg => {})
for (let i = 0; i < count; i++) {
await ipfs.pubsub.subscribe(someTopic, handlers[i])
}
for (let i = 0; i < count; i++) {
await ipfs.pubsub.unsubscribe(someTopic, handlers[i])
}
// Unsubscribing in the http client aborts the connection we hold open
// but does not wait for it to close so the subscription list sometimes
// takes a little time to empty
await waitFor(async () => {
const subs = await ipfs.pubsub.ls()
return subs.length === 0
}, {
interval: 1000,
timeout: 30000,
name: 'subscriptions to be empty'
})
})
it(`should subscribe ${count} handlers and unsubscribe once with no reference to the handlers`, async () => {
const someTopic = getTopic()
for (let i = 0; i < count; i++) {
await ipfs.pubsub.subscribe(someTopic, (msg) => {})
}
await ipfs.pubsub.unsubscribe(someTopic)
// Unsubscribing in the http client aborts the connection we hold open
// but does not wait for it to close so the subscription list sometimes
// takes a little time to empty
await waitFor(async () => {
const subs = await ipfs.pubsub.ls()
return subs.length === 0
}, {
interval: 1000,
timeout: 30000,
name: 'subscriptions to be empty'
})
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/pubsub/utils.js
================================================
import { nanoid } from 'nanoid'
import delay from 'delay'
/**
* @typedef {import('@libp2p/interface-peer-id').PeerId} PeerId
*/
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {string} topic
* @param {PeerId[]} peersToWait
* @param {number} waitForMs
* @returns
*/
export async function waitForPeers (ipfs, topic, peersToWait, waitForMs) {
const start = Date.now()
while (true) {
const peers = await ipfs.pubsub.peers(topic)
const everyPeerFound = peersToWait.every(p => peers.map(p => p.toString()).includes(p.toString()))
if (everyPeerFound) {
return
}
if (Date.now() > start + waitForMs) {
throw new Error(`Timed out waiting for peers to be subscribed to "${topic}"`)
}
await delay(10)
}
}
export function getTopic () {
return 'pubsub-tests-' + nanoid()
}
================================================
FILE: packages/interface-ipfs-core/src/refs-local.js
================================================
/* eslint-env mocha */
import { fixtures } from './utils/index.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from './utils/mocha.js'
import all from 'it-all'
import { importer } from 'ipfs-unixfs-importer'
import drain from 'it-drain'
import { CID } from 'multiformats/cid'
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import blockstore from './utils/blockstore-adapter.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testRefsLocal (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.refs.local', function () {
this.timeout(60 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get local refs', async function () {
/**
* @param {string} name
*/
const content = (name) => ({
path: `test-folder/${name}`,
content: fixtures.directory.files[name]
})
const dirs = [
content('pp.txt'),
content('holmes.txt')
]
const imported = await all(importer(dirs, blockstore(ipfs)))
// otherwise go-ipfs doesn't show them in the local refs
await drain(ipfs.pin.addAll(imported.map(i => ({ cid: i.cid }))))
const refs = await all(ipfs.refs.local())
const cids = refs.map(r => r.ref)
expect(
cids.find(cid => {
const multihash = CID.parse(cid).multihash.bytes
return uint8ArrayEquals(imported[0].cid.multihash.bytes, multihash)
})
).to.be.ok()
expect(
cids.find(cid => {
const multihash = CID.parse(cid).multihash.bytes
return uint8ArrayEquals(imported[1].cid.multihash.bytes, multihash)
})
).to.be.ok()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/refs.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from './utils/mocha.js'
import loadFixture from 'aegir/fixtures'
import { CID } from 'multiformats/cid'
import all from 'it-all'
import drain from 'it-drain'
import testTimeout from './utils/test-timeout.js'
import * as dagPB from '@ipld/dag-pb'
import { UnixFS } from 'ipfs-unixfs'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testRefs (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.refs', function () {
this.timeout(60 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
/** @type {CID} */
let pbRootCid
/** @type {CID} */
let dagRootCid
before(async () => {
ipfs = (await factory.spawn()).api
})
before(async function () {
pbRootCid = await loadPbContent(ipfs, getMockObjects())
})
before(async function () {
dagRootCid = await loadDagContent(ipfs, getMockObjects())
})
after(() => factory.clean())
for (const [name, options] of Object.entries(getRefsTests())) {
const { path, params, expected, expectError, expectTimeout } = options
// eslint-disable-next-line no-loop-func
it(name, async function () {
this.timeout(20 * 1000)
// Call out to IPFS
const p = (path ? path(pbRootCid) : pbRootCid)
if (expectTimeout) {
return expect(all(ipfs.refs(p, params))).to.eventually.be.rejected
.and.be.an.instanceOf(Error)
.and.to.have.property('name')
.to.eql('TimeoutError')
}
if (expectError) {
return expect(all(ipfs.refs(p, params))).to.be.eventually.rejected.and.be.an.instanceOf(Error)
}
const refs = await all(ipfs.refs(p, params))
// Sort the refs not to lock-in the iteration order
// Check there was no error and the refs match what was expected
expect(refs.map(r => r.ref).sort()).to.eql(expected.sort())
})
}
it('should respect timeout option when listing refs', () => {
return testTimeout(() => drain(ipfs.refs('/ipfs/QmPDqvcuA4AkhBLBuh2y49yhUB98rCnxPxa3eVNC1kAbS1/foo/bar/baz.txt', {
timeout: 1
})))
})
it('should get refs with cbor links', async function () {
this.timeout(20 * 1000)
// Call out to IPFS
const refs = await all(ipfs.refs(`/ipfs/${dagRootCid}`, { recursive: true }))
// Check the refs match what was expected
expect(refs.map(r => r.ref).sort()).to.eql([
'QmPDqvcuA4AkhBLBuh2y49yhUB98rCnxPxa3eVNC1kAbSC',
'QmVwtsLUHurA6wUirPSdGeEW5tfBEqenXpeRaqr8XN7bNY',
'QmXGL3ZdYV5rNLCfHe1QsFSQGekRFzgbBu1B3XGZ7DV9fd',
'QmcSVZRN5E814KkPy4EHnftNAR7htbFvVhRKKqFs4FBwDG',
'QmcSVZRN5E814KkPy4EHnftNAR7htbFvVhRKKqFs4FBwDG',
'QmdBcHbK7uDQav8YrHsfKju3EKn48knxjd96KRMFs3gtS9',
'QmeX96opBHZHLySMFoNiWS5msxjyX6rqtr3Rr1u7uxn7zJ',
'Qmf8MwTnY7VdcnF8WcoJ3GB24NmNd1HsGzuEWCtUYDP38x',
'bafyreiagelcmhfn33zuslkdo7fkes3dzcr2nju6meh75zm6vqklfqiojam',
'bafyreic2f6adq5tqnbrvwiqc3jkz2cf4tz3cz2rp6plpij2qaoufgsxwmi',
'bafyreidoqtyvflv5v4c3gd3izxvpq4flke55ayurbrnhsxh7z5wwjc6v6e',
'bafyreifs2ub2lnq6n2quqbi3zb5homs5iqlmm77b3am252cqzxiu7phwpy'
])
})
})
}
function getMockObjects () {
return {
animals: {
land: {
'african.txt': loadFixture('test/fixtures/refs-test/animals/land/african.txt', 'interface-ipfs-core'),
'americas.txt': loadFixture('test/fixtures/refs-test/animals/land/americas.txt', 'interface-ipfs-core'),
'australian.txt': loadFixture('test/fixtures/refs-test/animals/land/australian.txt', 'interface-ipfs-core')
},
sea: {
'atlantic.txt': loadFixture('test/fixtures/refs-test/animals/sea/atlantic.txt', 'interface-ipfs-core'),
'indian.txt': loadFixture('test/fixtures/refs-test/animals/sea/indian.txt', 'interface-ipfs-core')
}
},
fruits: {
'tropical.txt': loadFixture('test/fixtures/refs-test/fruits/tropical.txt', 'interface-ipfs-core')
},
'atlantic-animals': loadFixture('test/fixtures/refs-test/atlantic-animals', 'interface-ipfs-core'),
'mushroom.txt': loadFixture('test/fixtures/refs-test/mushroom.txt', 'interface-ipfs-core')
}
}
/**
* @returns {Record string | string[], params: { edges?: boolean, format?: string, recursive?: boolean, unique?: boolean, maxDepth?: number, timeout?: number }, expected: string[], expectError?: boolean, expectTimeout?: boolean }>}
*/
function getRefsTests () {
return {
'should print added files': {
params: {},
expected: [
'QmYEJ7qQNZUvBnv4SZ3rEbksagaan3sGvnUq948vSG8Z34',
'QmUXzZKa3xhTauLektUiK4GiogHskuz1c57CnnoP4TgYJD',
'QmYLvZrFn8KE2bcJ9UFhthScBVbbcXEgkJnnCBeKWYkpuQ',
'QmRfqT4uTUgFXhWbfBZm6eZxi2FQ8pqYK5tcWRyTZ7RcgY'
]
},
'should print files in edges format': {
params: { edges: true },
expected: [
'Qmd5MhNjx3NSZm3L2QKG1TFvqkTRbtZwGJinqEfqpfHH7s -> QmYEJ7qQNZUvBnv4SZ3rEbksagaan3sGvnUq948vSG8Z34',
'Qmd5MhNjx3NSZm3L2QKG1TFvqkTRbtZwGJinqEfqpfHH7s -> QmUXzZKa3xhTauLektUiK4GiogHskuz1c57CnnoP4TgYJD',
'Qmd5MhNjx3NSZm3L2QKG1TFvqkTRbtZwGJinqEfqpfHH7s -> QmYLvZrFn8KE2bcJ9UFhthScBVbbcXEgkJnnCBeKWYkpuQ',
'Qmd5MhNjx3NSZm3L2QKG1TFvqkTRbtZwGJinqEfqpfHH7s -> QmRfqT4uTUgFXhWbfBZm6eZxi2FQ8pqYK5tcWRyTZ7RcgY'
]
},
'should print files in custom format': {
params: { format: ': => ' },
expected: [
'animals: Qmd5MhNjx3NSZm3L2QKG1TFvqkTRbtZwGJinqEfqpfHH7s => QmYEJ7qQNZUvBnv4SZ3rEbksagaan3sGvnUq948vSG8Z34',
'atlantic-animals: Qmd5MhNjx3NSZm3L2QKG1TFvqkTRbtZwGJinqEfqpfHH7s => QmUXzZKa3xhTauLektUiK4GiogHskuz1c57CnnoP4TgYJD',
'fruits: Qmd5MhNjx3NSZm3L2QKG1TFvqkTRbtZwGJinqEfqpfHH7s => QmYLvZrFn8KE2bcJ9UFhthScBVbbcXEgkJnnCBeKWYkpuQ',
'mushroom.txt: Qmd5MhNjx3NSZm3L2QKG1TFvqkTRbtZwGJinqEfqpfHH7s => QmRfqT4uTUgFXhWbfBZm6eZxi2FQ8pqYK5tcWRyTZ7RcgY'
]
},
'should follow a path, /': {
path: (cid) => `/ipfs/${cid}/animals`,
params: { format: '' },
expected: [
'land',
'sea'
]
},
'should follow a path, //': {
path: (cid) => `/ipfs/${cid}/animals/land`,
params: { format: '' },
expected: [
'african.txt',
'americas.txt',
'australian.txt'
]
},
'should follow a path with recursion, /': {
path: (cid) => `/ipfs/${cid}/animals`,
params: { format: '', recursive: true },
expected: [
'land',
'african.txt',
'americas.txt',
'australian.txt',
'sea',
'atlantic.txt',
'indian.txt'
]
},
'should recursively follows folders, -r': {
params: { format: '', recursive: true },
expected: [
'animals',
'land',
'african.txt',
'americas.txt',
'australian.txt',
'sea',
'atlantic.txt',
'indian.txt',
'atlantic-animals',
'fruits',
'tropical.txt',
'mushroom.txt'
]
},
'should get refs with recursive and unique option': {
params: { format: '', recursive: true, unique: true },
expected: [
'QmRfqT4uTUgFXhWbfBZm6eZxi2FQ8pqYK5tcWRyTZ7RcgY',
'QmUXzZKa3xhTauLektUiK4GiogHskuz1c57CnnoP4TgYJD',
'QmVX54jfjB8eRxLVxyQSod6b1FyDh7mR4mQie9j97i2Qk3',
'QmWEuXAjUGyndgr4MKqMBgzMW36XgPgvitt2jsXgtuc7JE',
'QmYEJ7qQNZUvBnv4SZ3rEbksagaan3sGvnUq948vSG8Z34',
'QmYLvZrFn8KE2bcJ9UFhthScBVbbcXEgkJnnCBeKWYkpuQ',
'Qma5z9bmwPcrWLJxX6Vj6BrcybaFg84c2riNbUKrSVf8h1',
'QmbrFTo4s6H23W6wmoZKQC2vSogGeQ4dYiceSqJddzrKVa',
'QmdHVR8M4zAdGctnTYq4fyPZjTwwzdcBpGWAfMAhAVfT9n',
'Qmf6MrqT2oAve9diagLTMCYFPEcSx7fnUdW3xAjhXm32vo',
'QmfP6D9bRV4FEYDL4EHZtZG58kDwDfnzmyjuyK5d1pvzbM'
]
},
'should get refs with max depth of 1': {
params: { format: '', recursive: true, maxDepth: 1 },
expected: [
'animals',
'atlantic-animals',
'fruits',
'mushroom.txt'
]
},
'should get refs with max depth of 2': {
params: { format: '', recursive: true, maxDepth: 2 },
expected: [
'animals',
'land',
'sea',
'atlantic-animals',
'fruits',
'tropical.txt',
'mushroom.txt'
]
},
'should get refs with max depth of 3': {
params: { format: '', recursive: true, maxDepth: 3 },
expected: [
'animals',
'land',
'african.txt',
'americas.txt',
'australian.txt',
'sea',
'atlantic.txt',
'indian.txt',
'atlantic-animals',
'fruits',
'tropical.txt',
'mushroom.txt'
]
},
'should get refs with max depth of 0': {
params: { recursive: true, maxDepth: 0 },
expected: []
},
'should follow a path with max depth 1, /': {
path: (cid) => `/ipfs/${cid}/animals`,
params: { format: '', recursive: true, maxDepth: 1 },
expected: [
'land',
'sea'
]
},
'should follow a path with max depth 2, /': {
path: (cid) => `/ipfs/${cid}/animals`,
params: { format: '', recursive: true, maxDepth: 2 },
expected: [
'land',
'african.txt',
'americas.txt',
'australian.txt',
'sea',
'atlantic.txt',
'indian.txt'
]
},
'should print refs for multiple paths': {
path: (cid) => [`/ipfs/${cid}/animals`, `/ipfs/${cid}/fruits`],
params: { format: '', recursive: true },
expected: [
'land',
'african.txt',
'americas.txt',
'australian.txt',
'sea',
'atlantic.txt',
'indian.txt',
'tropical.txt'
]
},
'should not be able to specify edges and format': {
params: { format: '', edges: true },
expected: [],
expectError: true
},
'should print nothing for non-existent hashes': {
path: () => 'QmYmW4HiZhotsoSqnv2o1oSssvkRM8b9RweBoH7ao5nki2',
params: { timeout: 2000 },
expected: ['']
}
}
}
/**
* @typedef {object} Store
* @property {(data: Uint8Array) => Promise} putData
* @property {(links: { name: string, cid: string }[]) => Promise} putLinks
*/
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {any} node
*/
function loadPbContent (ipfs, node) {
/**
* @type {Store}
*/
const store = {
putData: (data) => {
return ipfs.block.put(
dagPB.encode({
Data: data,
Links: []
})
)
},
putLinks: (links) => {
return ipfs.block.put(dagPB.encode({
Links: links.map(({ name, cid }) => {
return {
Name: name,
Tsize: 8,
Hash: CID.parse(cid)
}
})
}))
}
}
return loadContent(ipfs, store, node)
}
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {any} node
*/
function loadDagContent (ipfs, node) {
/**
* @type {Store}
*/
const store = {
putData: (data) => {
const inner = new UnixFS({ type: 'file', data: data })
const serialized = dagPB.encode({
Data: inner.marshal(),
Links: []
})
return ipfs.block.put(serialized)
},
putLinks: (links) => {
/** @type {Record} */
const obj = {}
for (const { name, cid } of links) {
obj[name] = CID.parse(cid)
}
return ipfs.dag.put(obj)
}
}
return loadContent(ipfs, store, node)
}
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {Store} store
* @param {any} node
* @returns {Promise}
*/
async function loadContent (ipfs, store, node) {
if (node instanceof Uint8Array) {
return store.putData(node)
}
if (typeof node === 'object') {
const entries = Object.entries(node)
const sorted = entries.sort((a, b) => {
if (a[0] > b[0]) {
return 1
} else if (a[0] < b[0]) {
return -1
}
return 0
})
const res = await all((async function * () {
for (const [name, child] of sorted) {
const cid = await loadContent(ipfs, store, child)
yield { name, cid: cid && cid.toString() }
}
})())
return store.putLinks(res)
}
throw new Error('Please pass either data or object')
}
================================================
FILE: packages/interface-ipfs-core/src/repo/gc.js
================================================
/* eslint-env mocha */
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import all from 'it-all'
import drain from 'it-drain'
import { CID } from 'multiformats/cid'
import { base64 } from 'multiformats/bases/base64'
/**
* @param {import('ipfs-core-types').IPFS} ipfs
*/
async function getBaseEncodedMultihashes (ipfs) {
const refs = await all(ipfs.refs.local())
return refs.map(r => base64.encode(CID.parse(r.ref).multihash.bytes))
}
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {CID} cid
*/
async function shouldHaveRef (ipfs, cid) {
return expect(getBaseEncodedMultihashes(ipfs)).to.eventually.include(base64.encode(cid.multihash.bytes))
}
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {CID} cid
*/
async function shouldNotHaveRef (ipfs, cid) {
return expect(getBaseEncodedMultihashes(ipfs)).to.eventually.not.include(base64.encode(cid.multihash.bytes))
}
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testGc (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.repo.gc', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should run garbage collection', async () => {
const res = await ipfs.add(uint8ArrayFromString('apples'))
const pinset = await all(ipfs.pin.ls())
expect(pinset.map(obj => obj.cid.toString())).includes(res.cid.toString())
await ipfs.pin.rm(res.cid)
await all(ipfs.repo.gc())
const finalPinset = await all(ipfs.pin.ls())
expect(finalPinset.map(obj => obj.cid.toString())).not.includes(res.cid.toString())
})
it('should clean up unpinned data', async () => {
// Add some data. Note: this will implicitly pin the data, which causes
// some blocks to be added for the data itself and for the pinning
// information that refers to the blocks
const addRes = await ipfs.add(uint8ArrayFromString('apples'))
const cid = addRes.cid
// Get the list of local blocks after the add, should be bigger than
// the initial list and contain hash
await shouldHaveRef(ipfs, cid)
// Run garbage collection
await drain(ipfs.repo.gc())
// Get the list of local blocks after GC, should still contain the hash,
// because the file is still pinned
await shouldHaveRef(ipfs, cid)
// Unpin the data
await ipfs.pin.rm(cid)
// Run garbage collection
await all(ipfs.repo.gc())
// The list of local blocks should no longer contain the hash
await shouldNotHaveRef(ipfs, cid)
})
it('should clean up removed MFS files', async () => {
// Add a file to MFS
await ipfs.files.write('/test', uint8ArrayFromString('oranges'), { create: true })
const stats = await ipfs.files.stat('/test')
expect(stats.type).to.equal('file')
// Get the list of local blocks after the add, should be bigger than
// the initial list and contain hash
await shouldHaveRef(ipfs, stats.cid)
// Run garbage collection
await drain(ipfs.repo.gc())
// Get the list of local blocks after GC, should still contain the hash,
// because the file is in MFS
await shouldHaveRef(ipfs, stats.cid)
// Remove the file
await ipfs.files.rm('/test')
// Run garbage collection
await drain(ipfs.repo.gc())
// The list of local blocks should no longer contain the hash
await shouldNotHaveRef(ipfs, stats.cid)
})
it('should clean up block only after unpinned and removed from MFS', async () => {
// Add a file to MFS
await ipfs.files.write('/test', uint8ArrayFromString('peaches'), { create: true })
const stats = await ipfs.files.stat('/test')
expect(stats.type).to.equal('file')
const mfsFileCid = stats.cid
// Get the CID of the data in the file
const block = await ipfs.block.get(mfsFileCid)
// Add the data to IPFS (which implicitly pins the data)
const addRes = await ipfs.add(block)
const dataCid = addRes.cid
// Get the list of local blocks after the add, should be bigger than
// the initial list and contain the data hash
await shouldHaveRef(ipfs, dataCid)
// Run garbage collection
await drain(ipfs.repo.gc())
// Get the list of local blocks after GC, should still contain the hash,
// because the file is pinned and in MFS
await shouldHaveRef(ipfs, dataCid)
// Remove the file
await ipfs.files.rm('/test')
// Run garbage collection
await drain(ipfs.repo.gc())
// Get the list of local blocks after GC, should still contain the hash,
// because the file is still pinned
await shouldNotHaveRef(ipfs, mfsFileCid)
await shouldHaveRef(ipfs, dataCid)
// Unpin the data
await ipfs.pin.rm(dataCid)
// Run garbage collection
await drain(ipfs.repo.gc())
// The list of local blocks should no longer contain the hashes
await shouldNotHaveRef(ipfs, mfsFileCid)
await shouldNotHaveRef(ipfs, dataCid)
})
it('should clean up indirectly pinned data after recursive pin removal', async () => {
// Add some data
const addRes = await ipfs.add(uint8ArrayFromString('pears'))
const dataCid = addRes.cid
// Unpin the data
await ipfs.pin.rm(dataCid)
// Create a link to the data from an object
const obj = {
Data: uint8ArrayFromString('fruit'),
Links: [{
Name: 'p',
Hash: dataCid,
Tsize: addRes.size
}]
}
// Put the object into IPFS
const objCid = await ipfs.object.put(obj)
// Putting an object doesn't pin it
expect((await all(ipfs.pin.ls())).map(p => p.cid.toString())).not.includes(objCid.toString())
// Get the list of local blocks after the add, should be bigger than
// the initial list and contain data and object hash
await shouldHaveRef(ipfs, objCid)
await shouldHaveRef(ipfs, dataCid)
// Recursively pin the object
await ipfs.pin.add(objCid, { recursive: true })
// The data should now be indirectly pinned
const pins = await all(ipfs.pin.ls())
expect(pins.find(p => p.cid.toString() === dataCid.toString())).to.have.property('type', 'indirect')
// Run garbage collection
await drain(ipfs.repo.gc())
// Get the list of local blocks after GC, should still contain the data
// hash, because the data is still (indirectly) pinned
await shouldHaveRef(ipfs, objCid)
await shouldHaveRef(ipfs, dataCid)
// Recursively unpin the object
await ipfs.pin.rm(objCid.toString())
// Run garbage collection
await drain(ipfs.repo.gc())
// The list of local blocks should no longer contain the hashes
await shouldNotHaveRef(ipfs, objCid)
await shouldNotHaveRef(ipfs, dataCid)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/repo/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testVersion } from './version.js'
import { testStat } from './stat.js'
import { testGc } from './gc.js'
const tests = {
version: testVersion,
stat: testStat,
gc: testGc
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/repo/stat.js
================================================
/* eslint-env mocha */
import { expectIsRepo } from '../stats/utils.js'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testStat (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.repo.stat', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get repo stats', async () => {
const res = await ipfs.repo.stat()
expectIsRepo(null, res)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/repo/version.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testVersion (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.repo.version', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get the repo version', async () => {
const version = await ipfs.repo.version()
expect(version).to.exist()
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/stats/bitswap.js
================================================
/* eslint-env mocha */
import { getDescribe, getIt } from '../utils/mocha.js'
import { expectIsBitswap } from './utils.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testBitswap (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.stats.bitswap', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get bitswap stats', async () => {
const res = await ipfs.stats.bitswap()
expectIsBitswap(null, res)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/stats/bw.js
================================================
/* eslint-env mocha */
import { expectIsBandwidth } from './utils.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import last from 'it-last'
import all from 'it-all'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testBw (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.stats.bw', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get bandwidth stats ', async () => {
const res = await last(ipfs.stats.bw())
if (!res) {
throw new Error('No bw stats returned')
}
expectIsBandwidth(null, res)
})
it('should throw error for invalid interval option', async () => {
await expect(all(ipfs.stats.bw({ poll: true, interval: 'INVALID INTERVAL' })))
.to.eventually.be.rejected()
.with.property('message').that.matches(/invalid duration/)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/stats/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testBitswap } from './bitswap.js'
import { testBw } from './bw.js'
import { testRepo } from './repo.js'
const tests = {
bitswap: testBitswap,
bw: testBw,
repo: testRepo
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/stats/repo.js
================================================
/* eslint-env mocha */
import { expectIsRepo } from './utils.js'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testRepo (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.stats.repo', () => {
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should get repo stats', async () => {
const res = await ipfs.stats.repo()
expectIsRepo(null, res)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/stats/utils.js
================================================
import { expect } from 'aegir/chai'
/**
* @param {any} n
*/
const isBigInt = (n) => {
return typeof n === 'bigint'
}
/**
* @param {Error | null} err
* @param {import('ipfs-core-types/src/bitswap').Stats} stats
*/
export function expectIsBitswap (err, stats) {
expect(err).to.not.exist()
expect(stats).to.exist()
expect(stats).to.have.a.property('provideBufLen')
expect(stats).to.have.a.property('wantlist')
expect(stats).to.have.a.property('peers')
expect(stats).to.have.a.property('blocksReceived')
expect(stats).to.have.a.property('dataReceived')
expect(stats).to.have.a.property('blocksSent')
expect(stats).to.have.a.property('dataSent')
expect(stats).to.have.a.property('dupBlksReceived')
expect(stats).to.have.a.property('dupDataReceived')
expect(stats.provideBufLen).to.a('number')
expect(stats.wantlist).to.be.an('array')
expect(stats.peers).to.be.an('array')
expect(isBigInt(stats.blocksReceived)).to.eql(true)
expect(isBigInt(stats.dataReceived)).to.eql(true)
expect(isBigInt(stats.blocksSent)).to.eql(true)
expect(isBigInt(stats.dataSent)).to.eql(true)
expect(isBigInt(stats.dupBlksReceived)).to.eql(true)
expect(isBigInt(stats.dupDataReceived)).to.eql(true)
}
/**
* @param {Error | null} err
* @param {import('ipfs-core-types/src/stats').BWResult} stats
*/
export function expectIsBandwidth (err, stats) {
expect(err).to.not.exist()
expect(stats).to.exist()
expect(stats).to.have.a.property('totalIn')
expect(stats).to.have.a.property('totalOut')
expect(stats).to.have.a.property('rateIn')
expect(stats).to.have.a.property('rateOut')
expect(isBigInt(stats.totalIn)).to.eql(true)
expect(isBigInt(stats.totalOut)).to.eql(true)
expect(stats.rateIn).to.be.a('number')
expect(stats.rateOut).to.be.a('number')
}
/**
* @param {Error | null} err
* @param {import('ipfs-core-types/src/repo').StatResult} res
*/
export function expectIsRepo (err, res) {
expect(err).to.not.exist()
expect(res).to.exist()
expect(res).to.have.a.property('numObjects')
expect(res).to.have.a.property('repoSize')
expect(res).to.have.a.property('repoPath')
expect(res).to.have.a.property('version')
expect(res).to.have.a.property('storageMax')
expect(isBigInt(res.numObjects)).to.eql(true)
expect(isBigInt(res.repoSize)).to.eql(true)
expect(isBigInt(res.storageMax)).to.eql(true)
expect(res.repoPath).to.be.a('string')
expect(res.version).to.be.a('string')
}
================================================
FILE: packages/interface-ipfs-core/src/swarm/addrs.js
================================================
/* eslint-env mocha */
import { isMultiaddr } from '@multiformats/multiaddr'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { isWebWorker } from 'ipfs-utils/src/env.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testAddrs (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.swarm.addrs', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfsA
/** @type {import('ipfs-core-types').IPFS} */
let ipfsB
/** @type {import('ipfs-core-types/src/root').IDResult} */
let ipfsBId
before(async () => {
ipfsA = (await factory.spawn({ type: 'proc' })).api
// webworkers are not dialable because webrtc is not available
ipfsB = (await factory.spawn({ type: isWebWorker ? 'go' : undefined })).api
ipfsBId = await ipfsB.id()
await ipfsA.swarm.connect(ipfsBId.addresses[0])
})
after(() => factory.clean())
it('should get a list of node addresses', async () => {
const peers = await ipfsA.swarm.addrs()
expect(peers).to.not.be.empty()
expect(peers).to.be.an('array')
for (const peer of peers) {
expect(peer.id).to.be.ok()
expect(peer).to.have.a.property('addrs').that.is.an('array')
for (const ma of peer.addrs) {
expect(isMultiaddr(ma)).to.be.true()
}
}
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/swarm/connect.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { isWebWorker } from 'ipfs-utils/src/env.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testConnect (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.swarm.connect', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfsA
/** @type {import('ipfs-core-types').IPFS} */
let ipfsB
/** @type {import('ipfs-core-types/src/root').IDResult} */
let ipfsBId
before(async () => {
ipfsA = (await factory.spawn({ type: 'proc' })).api
// webworkers are not dialable because webrtc is not available
ipfsB = (await factory.spawn({ type: isWebWorker ? 'go' : undefined })).api
ipfsBId = await ipfsB.id()
})
after(() => factory.clean())
it('should connect to a peer', async () => {
let peers
peers = await ipfsA.swarm.peers()
expect(peers.map(p => p.peer.toString())).to.not.include(ipfsBId.id.toString())
await ipfsA.swarm.connect(ipfsBId.addresses[0])
peers = await ipfsA.swarm.peers()
expect(peers.map(p => p.peer.toString())).to.include(ipfsBId.id.toString())
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/swarm/disconnect.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { isWebWorker } from 'ipfs-utils/src/env.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testDisconnect (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.swarm.disconnect', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfsA
/** @type {import('ipfs-core-types').IPFS} */
let ipfsB
/** @type {import('ipfs-core-types/src/root').IDResult} */
let ipfsBId
before(async () => {
ipfsA = (await factory.spawn({ type: 'proc' })).api
// webworkers are not dialable because webrtc is not available
ipfsB = (await factory.spawn({ type: isWebWorker ? 'go' : undefined })).api
ipfsBId = await ipfsB.id()
})
beforeEach(async () => {
await ipfsA.swarm.connect(ipfsBId.addresses[0])
})
after(() => factory.clean())
it('should disconnect from a peer', async () => {
let peers
peers = await ipfsA.swarm.peers()
expect(peers).to.have.length.above(0)
await ipfsA.swarm.disconnect(ipfsBId.addresses[0])
peers = await ipfsA.swarm.peers()
expect(peers).to.have.length(0)
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/swarm/index.js
================================================
import { createSuite } from '../utils/suite.js'
import { testConnect } from './connect.js'
import { testPeers } from './peers.js'
import { testAddrs } from './addrs.js'
import { testLocalAddrs } from './local-addrs.js'
import { testDisconnect } from './disconnect.js'
const tests = {
connect: testConnect,
peers: testPeers,
addrs: testAddrs,
localAddrs: testLocalAddrs,
disconnect: testDisconnect
}
export default createSuite(tests)
================================================
FILE: packages/interface-ipfs-core/src/swarm/local-addrs.js
================================================
/* eslint-env mocha */
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
import { isWebWorker } from 'ipfs-utils/src/env.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {Factory} factory
* @param {object} options
*/
export function testLocalAddrs (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.swarm.localAddrs', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfs
before(async () => {
ipfs = (await factory.spawn()).api
})
after(() => factory.clean())
it('should list local addresses the node is listening on', async () => {
const multiaddrs = await ipfs.swarm.localAddrs()
expect(multiaddrs).to.be.an.instanceOf(Array)
if (isWebWorker && factory.opts.type === 'proc') {
expect(multiaddrs).to.have.lengthOf(0)
} else {
expect(multiaddrs).to.not.be.empty()
}
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/swarm/peers.js
================================================
/* eslint-env mocha */
import { isMultiaddr } from '@multiformats/multiaddr'
import delay from 'delay'
import { isBrowser, isWebWorker } from 'ipfs-utils/src/env.js'
import { expect } from 'aegir/chai'
import { getDescribe, getIt } from '../utils/mocha.js'
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
*/
/**
* @param {import('ipfs-core-types/src/swarm').PeersResult[]} peers
*/
function peersAreUnique (peers) {
const peerSet = new Set()
peers.forEach(peer => {
peerSet.add(peer.peer.toString())
})
expect(peerSet).to.have.lengthOf(peers.length)
}
/**
* @param {Factory} factory
* @param {object} options
*/
export function testPeers (factory, options) {
const describe = getDescribe(options)
const it = getIt(options)
describe('.swarm.peers', function () {
this.timeout(80 * 1000)
/** @type {import('ipfs-core-types').IPFS} */
let ipfsA
/** @type {import('ipfs-core-types').IPFS} */
let ipfsB
/** @type {import('ipfs-core-types/src/root').IDResult} */
let ipfsBId
before(async () => {
ipfsA = (await factory.spawn({ type: 'proc' })).api
ipfsB = (await factory.spawn({ type: isWebWorker ? 'go' : undefined })).api
ipfsBId = await ipfsB.id()
await ipfsA.swarm.connect(ipfsBId.addresses[0])
/* TODO: Seen if we still need this after this is fixed
https://github.com/ipfs/js-ipfs/issues/2601 gets resolved */
// await delay(60 * 1000) // wait for open streams in the connection available
})
after(() => factory.clean())
it('should list peers this node is connected to', async () => {
const peers = await ipfsA.swarm.peers()
expect(peers).to.have.length.above(0)
const peer = peers[0]
expect(peer).to.have.a.property('addr')
expect(isMultiaddr(peer.addr)).to.equal(true)
expect(peer).to.have.a.property('peer')
expect(peer.peer).to.be.ok()
expect(peer).to.not.have.a.property('latency')
/* TODO: These assertions must be uncommented as soon as
https://github.com/ipfs/js-ipfs/issues/2601 gets resolved */
// expect(peer).to.have.a.property('muxer')
// expect(peer).to.not.have.a.property('streams')
})
it('should list peers this node is connected to with verbose option', async () => {
const peers = await ipfsA.swarm.peers({ verbose: true })
expect(peers).to.have.length.above(0)
const peer = peers[0]
expect(peer).to.have.a.property('addr')
expect(isMultiaddr(peer.addr)).to.equal(true)
expect(peer).to.have.a.property('peer')
expect(peer).to.have.a.property('latency')
expect(peer.latency).to.match(/n\/a|[0-9]+[mµ]?s/) // n/a or 3ms or 3µs or 3s
/* TODO: These assertions must be uncommented as soon as
https://github.com/ipfs/js-ipfs/issues/2601 gets resolved */
// expect(peer).to.have.a.property('muxer')
// expect(peer).to.have.a.property('streams')
})
/**
* @param {string | string[]} addrs
* @returns
*/
function getConfig (addrs) {
addrs = Array.isArray(addrs) ? addrs : [addrs]
return {
Addresses: {
Swarm: addrs,
API: '/ip4/127.0.0.1/tcp/0',
Gateway: '/ip4/127.0.0.1/tcp/0'
},
Bootstrap: [],
Discovery: {
MDNS: {
Enabled: false
}
}
}
}
it('should list peers only once', async () => {
const nodeA = (await factory.spawn({ type: 'proc' })).api
const nodeB = (await factory.spawn({ type: isWebWorker ? 'go' : undefined })).api
const nodeBId = await nodeB.id()
await nodeA.swarm.connect(nodeBId.addresses[0])
await delay(1000)
peersAreUnique(await nodeA.swarm.peers())
peersAreUnique(await nodeB.swarm.peers())
})
it('should list peers only once even if they have multiple addresses', async () => {
// TODO: Change to port 0, needs: https://github.com/ipfs/interface-ipfs-core/issues/152
const config = getConfig(isBrowser && factory.opts.type !== 'go'
? [
`${process.env.SIGNALA_SERVER}`,
`${process.env.SIGNALB_SERVER}`
]
: [
'/ip4/127.0.0.1/tcp/26545/ws',
'/ip4/127.0.0.1/tcp/26546/ws'
])
const nodeA = (await factory.spawn({
// browser nodes have webrtc-star addresses which can't be dialled by go so make the other
// peer a js-ipfs node to get a tcp address that can be dialled. Also, webworkers are not
// diable so don't use a in-proc node for webworkers
type: ((isBrowser && factory.opts.type === 'go') || isWebWorker) ? 'js' : 'proc'
})).api
const nodeAId = await nodeA.id()
const nodeB = (await factory.spawn({
type: isWebWorker ? 'go' : undefined,
ipfsOptions: {
config
}
})).api
await nodeB.swarm.connect(nodeAId.addresses[0])
await delay(1000)
peersAreUnique(await nodeA.swarm.peers())
peersAreUnique(await nodeB.swarm.peers())
})
})
}
================================================
FILE: packages/interface-ipfs-core/src/utils/blockstore-adapter.js
================================================
import { BaseBlockstore } from 'blockstore-core/base'
import * as raw from 'multiformats/codecs/raw'
import * as dagPB from '@ipld/dag-pb'
import * as dagCBOR from '@ipld/dag-cbor'
import { sha256 } from 'multiformats/hashes/sha2'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
/**
* @type {Record}
*/
const formats = {
[raw.code]: raw.name,
[dagPB.code]: dagPB.name,
[dagCBOR.code]: dagCBOR.name
}
/**
* @type {Record}
*/
const hashes = {
[sha256.code]: sha256.name
}
class IPFSBlockstore extends BaseBlockstore {
/**
* @param {import('ipfs-core-types').IPFS} ipfs
*/
constructor (ipfs) {
super()
this.ipfs = ipfs
}
/**
* @param {import('multiformats/cid').CID} cid
* @param {Uint8Array} buf
*/
async put (cid, buf) {
const c = await this.ipfs.block.put(buf, {
format: formats[cid.code],
mhtype: hashes[cid.multihash.code],
version: cid.version
})
if (uint8ArrayToString(c.multihash.bytes, 'base64') !== uint8ArrayToString(cid.multihash.bytes, 'base64')) {
throw new Error('Multihashes of stored blocks did not match')
}
}
}
/**
* @param {import('ipfs-core-types').IPFS} ipfs
*/
export default function createBlockstore (ipfs) {
return new IPFSBlockstore(ipfs)
}
================================================
FILE: packages/interface-ipfs-core/src/utils/create-sharded-directory.js
================================================
import { expect } from 'aegir/chai'
import isShardAtPath from './is-shard-at-path.js'
import last from 'it-last'
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {number} [files]
*/
export async function createShardedDirectory (ipfs, files = 1001) {
const dirPath = `/sharded-dir-${Math.random()}`
const result = await last(ipfs.addAll((function * () {
for (let i = 0; i < files; i++) {
yield {
path: `${dirPath}/file-${i}`,
content: Uint8Array.from([0, 1, 2, 3, 4, 5, i])
}
}
}()), {
preload: false,
pin: false
}))
if (!result) {
throw new Error('No result received from ipfs.addAll')
}
await ipfs.files.cp(`/ipfs/${result.cid}`, dirPath)
await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true('Tried to create a shared directory but the result was not a shard')
return dirPath
}
================================================
FILE: packages/interface-ipfs-core/src/utils/create-two-shards.js
================================================
import { expect } from 'aegir/chai'
import isShardAtPath from './is-shard-at-path.js'
import last from 'it-last'
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {number} fileCount
*/
export async function createTwoShards (ipfs, fileCount) {
const dirPath = `/sharded-dir-${Math.random()}`
const files = new Array(fileCount).fill(0).map((_, index) => ({
path: `${dirPath}/file-${index}`,
content: Uint8Array.from([0, 1, 2, 3, 4, index])
}))
files[files.length - 1].path = `${dirPath}/file-${fileCount - 1}`
const allFiles = files.map(file => ({
...file
}))
const someFiles = files.map(file => ({
...file
}))
const nextFile = someFiles.pop()
if (!nextFile) {
throw new Error('No nextFile found')
}
const res1 = await last(ipfs.addAll(allFiles, {
// for js-ipfs - go-ipfs shards everything when sharding is turned on
shardSplitThreshold: files.length - 1,
preload: false,
pin: false
}))
if (!res1) {
throw new Error('No result received from ipfs.addAll')
}
const { cid: dirWithAllFiles } = res1
const res2 = await last(ipfs.addAll(someFiles, {
// for js-ipfs - go-ipfs shards everything when sharding is turned on
shardSplitThreshold: files.length - 1,
preload: false,
pin: false
}))
if (!res2) {
throw new Error('No result received from ipfs.addAll')
}
const { cid: dirWithSomeFiles } = res2
await expect(isShardAtPath(`/ipfs/${dirWithAllFiles}`, ipfs)).to.eventually.be.true()
await expect(isShardAtPath(`/ipfs/${dirWithSomeFiles}`, ipfs)).to.eventually.be.true()
return {
nextFile,
dirWithAllFiles,
dirWithSomeFiles,
dirPath
}
}
================================================
FILE: packages/interface-ipfs-core/src/utils/dump-shard.js
================================================
import { UnixFS } from 'ipfs-unixfs'
/**
* @param {string} path
* @param {import('ipfs-core-types').IPFS} ipfs
*/
export default async function dumpShard (path, ipfs) {
const stats = await ipfs.files.stat(path)
const { value: node } = await ipfs.dag.get(stats.cid)
const entry = UnixFS.unmarshal(node.Data)
if (entry.type !== 'hamt-sharded-directory') {
throw new Error('Not a shard')
}
await dumpSubShard(stats.cid, ipfs)
}
/**
* @param {import('multiformats/cid').CID} cid
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {string} prefix
*/
async function dumpSubShard (cid, ipfs, prefix = '') {
const { value: node } = await ipfs.dag.get(cid)
const entry = UnixFS.unmarshal(node.Data)
if (entry.type !== 'hamt-sharded-directory') {
throw new Error('Not a shard')
}
for (const link of node.Links) {
const { value: subNode } = await ipfs.dag.get(link.Hash)
const subEntry = UnixFS.unmarshal(subNode.Data)
console.info(`${prefix}${link.Name}`, ' ', subEntry.type) // eslint-disable-line no-console
if (link.Name.length === 2) {
await dumpSubShard(link.Hash, ipfs, `${prefix} `)
}
}
}
================================================
FILE: packages/interface-ipfs-core/src/utils/index.js
================================================
import { CID } from 'multiformats/cid'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import loadFixture from 'aegir/fixtures'
const ONE_MEG = Math.pow(2, 20)
export const fixtures = Object.freeze({
directory: Object.freeze({
cid: CID.parse('QmVvjDy7yF7hdnqE8Hrf4MHo5ABDtb5AbX6hWbD3Y42bXP'),
/** @type {Record} */
files: Object.freeze({
'pp.txt': loadFixture('test/fixtures/test-folder/pp.txt', 'interface-ipfs-core'),
'holmes.txt': loadFixture('test/fixtures/test-folder/holmes.txt', 'interface-ipfs-core'),
'jungle.txt': loadFixture('test/fixtures/test-folder/jungle.txt', 'interface-ipfs-core'),
'alice.txt': loadFixture('test/fixtures/test-folder/alice.txt', 'interface-ipfs-core'),
'files/hello.txt': loadFixture('test/fixtures/test-folder/files/hello.txt', 'interface-ipfs-core'),
'files/ipfs.txt': loadFixture('test/fixtures/test-folder/files/ipfs.txt', 'interface-ipfs-core')
})
}),
smallFile: Object.freeze({
cid: CID.parse('Qma4hjFTnCasJ8PVp3mZbZK5g2vGDT4LByLJ7m8ciyRFZP'),
data: uint8ArrayFromString('Plz add me!\n')
}),
bigFile: Object.freeze({
cid: CID.parse('QmcKEs7mbxbGPPc2zo77E6CPwgaSbY4SmD2MFh16AqaR9e'),
data: Uint8Array.from(new Array(ONE_MEG * 15).fill(0))
}),
emptyFile: Object.freeze({
cid: CID.parse('QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH'),
data: new Uint8Array(0)
})
})
================================================
FILE: packages/interface-ipfs-core/src/utils/ipfs-options-websockets-filter-all.js
================================================
import { webSockets } from '@libp2p/websockets'
import { all } from '@libp2p/websockets/filters'
export function ipfsOptionsWebsocketsFilterAll () {
return {
libp2p: {
config: {
transports: [
webSockets({
filter: all
})
]
}
}
}
}
================================================
FILE: packages/interface-ipfs-core/src/utils/is-shard-at-path.js
================================================
import { UnixFS } from 'ipfs-unixfs'
/**
* @param {string} path
* @param {import('ipfs-core-types').IPFS} ipfs
*/
export default async function isShardAtPath (path, ipfs) {
const stats = await ipfs.files.stat(path)
const { value: node } = await ipfs.dag.get(stats.cid)
const entry = UnixFS.unmarshal(node.Data)
return entry.type === 'hamt-sharded-directory'
}
================================================
FILE: packages/interface-ipfs-core/src/utils/mocha.js
================================================
/* eslint-env mocha */
/**
* @typedef {object} Skip
* @property {string} [name]
* @property {string} [reason]
*/
/**
* @param {any} o
* @returns {o is Skip}
*/
const isSkip = (o) => Object.prototype.toString.call(o) === '[object Object]' && (o.name || o.reason)
/**
* Get a "describe" function that is optionally 'skipped' or 'onlyed'
* If skip/only are boolean true, or an object with a reason property, then we
* want to skip/only the whole suite
*
* @param {object} [config]
* @param {boolean | Skip | (string | Skip)[]} [config.skip]
* @param {boolean} [config.only]
*/
export function getDescribe (config) {
if (config) {
if (config.skip === true) {
return describe.skip
}
if (config.only === true) {
return describe.only // eslint-disable-line
}
if (Array.isArray(config.skip)) {
const skipArr = config.skip
/**
* @param {string} name
* @param {*} impl
*/
const _describe = (name, impl) => {
const skip = skipArr.find(skip => {
if (typeof skip === 'string') {
return skip === name
}
return skip.name === name
})
if (skip) {
return describe.skip(`${name} (${typeof skip === 'string' ? '🤷' : skip.reason})`, impl)
}
describe(name, impl)
}
_describe.skip = describe.skip
_describe.only = describe.only // eslint-disable-line
return _describe
} else if (isSkip(config.skip)) {
const skip = config.skip
if (!skip.reason) {
return describe.skip
}
/**
* @param {string} name
* @param {*} impl
*/
const _describe = (name, impl) => {
describe.skip(`${name} (${skip.reason})`, impl)
}
_describe.skip = describe.skip
_describe.only = describe.only // eslint-disable-line
return _describe
}
}
return describe
}
/**
* Get an "it" function that is optionally 'skipped' or 'onlyed'
* If skip/only is an array, then we _might_ want to skip/only the specific
* test if one of the items in the array is the same as the test name or if one
* of the items in the array is an object with a name property that is the same
* as the test name.
*
* @param {object} [config]
* @param {boolean | Skip | (string | Skip)[]} [config.skip]
* @param {boolean} [config.only]
* @returns {Mocha.TestFunction}
*/
export function getIt (config) {
if (!config) return it
/**
* @param {string | Mocha.Func} name
* @param {Mocha.Func | Mocha.AsyncFunc} [impl]
*/
const _it = (name, impl) => {
if (typeof name !== 'string') {
throw new Error('Pass test name as first argument to it')
}
if (Array.isArray(config.skip)) {
const skip = config.skip
.map((s) => isSkip(s) ? s : { name: s, reason: '🤷' })
.find((s) => s.name === name)
if (skip) {
if (skip.reason) name = `${name} (${skip.reason})`
return it.skip(name, impl)
}
}
if (Array.isArray(config.only)) {
const only = config.only
.map((o) => isSkip(o) ? o : { name: o, reason: '🤷' })
.find((o) => o.name === name)
if (only) {
if (only.reason) name = `${name} (${only.reason})`
return it.only(name, impl) // eslint-disable-line no-only-tests/no-only-tests
}
}
return it(name, impl)
}
_it.retries = it.retries
_it.skip = it.skip
_it.only = it.only // eslint-disable-line no-only-tests/no-only-tests
return _it
}
================================================
FILE: packages/interface-ipfs-core/src/utils/suite.js
================================================
/**
* @typedef {import('ipfsd-ctl').Factory} Factory
* @typedef {object} Skip
* @property {string} [name]
* @property {string} [reason]
*/
/**
* @param {any} o
* @returns {o is Skip}
*/
const isSkip = (o) => Object.prototype.toString.call(o) === '[object Object]' && (o.name || o.reason)
/**
* @param {*} tests
* @param {*} [parent]
*/
export function createSuite (tests, parent) {
/**
* @param {Factory} factory
* @param {object} [options]
* @param {boolean | Skip | (string | Skip)[]} [options.skip]
* @param {boolean} [options.only]
*/
const suite = (factory, options = {}) => {
Object.keys(tests).forEach(t => {
const opts = Object.assign({}, options)
const suiteName = parent ? `${parent}.${t}` : t
if (Array.isArray(opts.skip)) {
const skip = opts.skip
.map((s) => isSkip(s) ? s : { name: s, reason: '🤷' })
.find((s) => s.name === suiteName)
if (skip) {
opts.skip = skip
}
}
if (Array.isArray(opts.only)) {
if (opts.only.includes(suiteName)) {
opts.only = true
}
}
tests[t](factory, opts)
})
}
return Object.assign(suite, tests)
}
================================================
FILE: packages/interface-ipfs-core/src/utils/test-timeout.js
================================================
import drain from 'it-drain'
/**
* @param {*} fn
* @returns {Promise}
*/
export default function testTimeout (fn) {
return new Promise((resolve, reject) => {
// some operations are either synchronous so cannot time out, or complete during
// processing of the microtask queue so the timeout timer doesn't fire. If this
// is the case this is more of a best-effort test..
setTimeout(() => {
const start = Date.now()
let res = fn()
if (res[Symbol.asyncIterator]) {
res = drain(res)
}
res.then((/** @type {*} */ result) => {
const timeTaken = Date.now() - start
if (timeTaken < 100) {
// the implementation may be too fast to measure a time out reliably on node
// due to the event loop being blocked. if it took longer than 100ms though,
// it almost certainly did not time out
return resolve()
}
reject(new Error(`API call did not time out after ${timeTaken}ms, got ${JSON.stringify(result, null, 2)}`))
}, (/** @type {Error} */ err) => {
if (err.toString().includes('Timeout')) {
return resolve()
}
const timeTaken = Date.now() - start
reject(new Error(`Expected TimeoutError after ${timeTaken}ms, got ${err.stack}`))
})
}, 10)
})
}
================================================
FILE: packages/interface-ipfs-core/src/utils/traverse-leaf-nodes.js
================================================
/**
* @typedef {import('@ipld/dag-pb').PBNode} PBNode
* @typedef {import('multiformats/cid').CID} CID
*/
/**
* @param {import('ipfs-core-types').IPFS} ipfs
* @param {CID} cid
*/
export async function * traverseLeafNodes (ipfs, cid) {
/**
* @param {import('multiformats/cid').CID} cid
* @returns {AsyncIterable<{ node: PBNode, cid: CID }>}
*/
async function * traverse (cid) {
const { value: node } = await ipfs.dag.get(cid)
if (node instanceof Uint8Array || !node.Links.length) {
yield {
node,
cid
}
return
}
for (const link of node.Links) {
yield * traverse(link.Hash)
}
}
yield * traverse(cid)
}
================================================
FILE: packages/interface-ipfs-core/src/utils/wait-for.js
================================================
import delay from 'delay'
import errCode from 'err-code'
/**
* Wait for async function `test` to resolve true or timeout after options.timeout milliseconds.
*
* @param {() => Promise | boolean} test
* @param {object} [options]
* @param {number} [options.timeout]
* @param {number} [options.interval]
* @param {string} [options.name]
*/
export default async function waitFor (test, options) {
const opts = Object.assign({ timeout: 5000, interval: 0, name: 'event' }, options)
const start = Date.now()
while (true) {
if (await test()) {
return
}
if (Date.now() > start + opts.timeout) {
throw errCode(new Error(`Timed out waiting for ${opts.name}`), 'ERR_TIMEOUT')
}
await delay(opts.interval)
}
}
================================================
FILE: packages/interface-ipfs-core/test/fixtures/.gitattributes
================================================
* -text
================================================
FILE: packages/interface-ipfs-core/test/fixtures/hidden-files-folder/.hiddenTest.txt
================================================
Aha! You found me!
================================================
FILE: packages/interface-ipfs-core/test/fixtures/hidden-files-folder/alice.txt
================================================
CHAPTER XII. Alice's Evidence
'Here!' cried Alice, quite forgetting in the flurry of the moment how
large she had grown in the last few minutes, and she jumped up in such
a hurry that she tipped over the jury-box with the edge of her skirt,
upsetting all the jurymen on to the heads of the crowd below, and there
they lay sprawling about, reminding her very much of a globe of goldfish
she had accidentally upset the week before.
'Oh, I BEG your pardon!' she exclaimed in a tone of great dismay, and
began picking them up again as quickly as she could, for the accident of
the goldfish kept running in her head, and she had a vague sort of idea
that they must be collected at once and put back into the jury-box, or
they would die.
'The trial cannot proceed,' said the King in a very grave voice, 'until
all the jurymen are back in their proper places--ALL,' he repeated with
great emphasis, looking hard at Alice as he said do.
Alice looked at the jury-box, and saw that, in her haste, she had put
the Lizard in head downwards, and the poor little thing was waving its
tail about in a melancholy way, being quite unable to move. She soon got
it out again, and put it right; 'not that it signifies much,' she said
to herself; 'I should think it would be QUITE as much use in the trial
one way up as the other.'
As soon as the jury had a little recovered from the shock of being
upset, and their slates and pencils had been found and handed back to
them, they set to work very diligently to write out a history of the
accident, all except the Lizard, who seemed too much overcome to do
anything but sit with its mouth open, gazing up into the roof of the
court.
'What do you know about this business?' the King said to Alice.
'Nothing,' said Alice.
'Nothing WHATEVER?' persisted the King.
'Nothing whatever,' said Alice.
'That's very important,' the King said, turning to the jury. They were
just beginning to write this down on their slates, when the White Rabbit
interrupted: 'UNimportant, your Majesty means, of course,' he said in a
very respectful tone, but frowning and making faces at him as he spoke.
'UNimportant, of course, I meant,' the King hastily said, and went on
to himself in an undertone,
'important--unimportant--unimportant--important--' as if he were trying
which word sounded best.
Some of the jury wrote it down 'important,' and some 'unimportant.'
Alice could see this, as she was near enough to look over their slates;
'but it doesn't matter a bit,' she thought to herself.
At this moment the King, who had been for some time busily writing in
his note-book, cackled out 'Silence!' and read out from his book, 'Rule
Forty-two. ALL PERSONS MORE THAN A MILE HIGH TO LEAVE THE COURT.'
Everybody looked at Alice.
'I'M not a mile high,' said Alice.
'You are,' said the King.
'Nearly two miles high,' added the Queen.
'Well, I shan't go, at any rate,' said Alice: 'besides, that's not a
regular rule: you invented it just now.'
'It's the oldest rule in the book,' said the King.
'Then it ought to be Number One,' said Alice.
The King turned pale, and shut his note-book hastily. 'Consider your
verdict,' he said to the jury, in a low, trembling voice.
'There's more evidence to come yet, please your Majesty,' said the White
Rabbit, jumping up in a great hurry; 'this paper has just been picked
up.'
'What's in it?' said the Queen.
'I haven't opened it yet,' said the White Rabbit, 'but it seems to be a
letter, written by the prisoner to--to somebody.'
'It must have been that,' said the King, 'unless it was written to
nobody, which isn't usual, you know.'
'Who is it directed to?' said one of the jurymen.
'It isn't directed at all,' said the White Rabbit; 'in fact, there's
nothing written on the OUTSIDE.' He unfolded the paper as he spoke, and
added 'It isn't a letter, after all: it's a set of verses.'
'Are they in the prisoner's handwriting?' asked another of the jurymen.
'No, they're not,' said the White Rabbit, 'and that's the queerest thing
about it.' (The jury all looked puzzled.)
'He must have imitated somebody else's hand,' said the King. (The jury
all brightened up again.)
'Please your Majesty,' said the Knave, 'I didn't write it, and they
can't prove I did: there's no name signed at the end.'
'If you didn't sign it,' said the King, 'that only makes the matter
worse. You MUST have meant some mischief, or else you'd have signed your
name like an honest man.'
There was a general clapping of hands at this: it was the first really
clever thing the King had said that day.
'That PROVES his guilt,' said the Queen.
'It proves nothing of the sort!' said Alice. 'Why, you don't even know
what they're about!'
'Read them,' said the King.
The White Rabbit put on his spectacles. 'Where shall I begin, please
your Majesty?' he asked.
'Begin at the beginning,' the King said gravely, 'and go on till you
come to the end: then stop.'
These were the verses the White Rabbit read:--
'They told me you had been to her,
And mentioned me to him:
She gave me a good character,
But said I could not swim.
He sent them word I had not gone
(We know it to be true):
If she should push the matter on,
What would become of you?
I gave her one, they gave him two,
You gave us three or more;
They all returned from him to you,
Though they were mine before.
If I or she should chance to be
Involved in this affair,
He trusts to you to set them free,
Exactly as we were.
My notion was that you had been
(Before she had this fit)
An obstacle that came between
Him, and ourselves, and it.
Don't let him know she liked them best,
For this must ever be
A secret, kept from all the rest,
Between yourself and me.'
'That's the most important piece of evidence we've heard yet,' said the
King, rubbing his hands; 'so now let the jury--'
'If any one of them can explain it,' said Alice, (she had grown so large
in the last few minutes that she wasn't a bit afraid of interrupting
him,) 'I'll give him sixpence. _I_ don't believe there's an atom of
meaning in it.'
The jury all wrote down on their slates, 'SHE doesn't believe there's an
atom of meaning in it,' but none of them attempted to explain the paper.
'If there's no meaning in it,' said the King, 'that saves a world of
trouble, you know, as we needn't try to find any. And yet I don't know,'
he went on, spreading out the verses on his knee, and looking at them
with one eye; 'I seem to see some meaning in them, after all. "--SAID
I COULD NOT SWIM--" you can't swim, can you?' he added, turning to the
Knave.
The Knave shook his head sadly. 'Do I look like it?' he said. (Which he
certainly did NOT, being made entirely of cardboard.)
'All right, so far,' said the King, and he went on muttering over
the verses to himself: '"WE KNOW IT TO BE TRUE--" that's the jury, of
course--"I GAVE HER ONE, THEY GAVE HIM TWO--" why, that must be what he
did with the tarts, you know--'
'But, it goes on "THEY ALL RETURNED FROM HIM TO YOU,"' said Alice.
'Why, there they are!' said the King triumphantly, pointing to the tarts
on the table. 'Nothing can be clearer than THAT. Then again--"BEFORE SHE
HAD THIS FIT--" you never had fits, my dear, I think?' he said to the
Queen.
'Never!' said the Queen furiously, throwing an inkstand at the Lizard
as she spoke. (The unfortunate little Bill had left off writing on his
slate with one finger, as he found it made no mark; but he now hastily
began again, using the ink, that was trickling down his face, as long as
it lasted.)
'Then the words don't FIT you,' said the King, looking round the court
with a smile. There was a dead silence.
'It's a pun!' the King added in an offended tone, and everybody laughed,
'Let the jury consider their verdict,' the King said, for about the
twentieth time that day.
'No, no!' said the Queen. 'Sentence first--verdict afterwards.'
'Stuff and nonsense!' said Alice loudly. 'The idea of having the
sentence first!'
'Hold your tongue!' said the Queen, turning purple.
'I won't!' said Alice.
'Off with her head!' the Queen shouted at the top of her voice. Nobody
moved.
'Who cares for you?' said Alice, (she had grown to her full size by this
time.) 'You're nothing but a pack of cards!'
At this the whole pack rose up into the air, and came flying down upon
her: she gave a little scream, half of fright and half of anger, and
tried to beat them off, and found herself lying on the bank, with her
head in the lap of her sister, who was gently brushing away some dead
leaves that had fluttered down from the trees upon her face.
'Wake up, Alice dear!' said her sister; 'Why, what a long sleep you've
had!'
'Oh, I've had such a curious dream!' said Alice, and she told her
sister, as well as she could remember them, all these strange Adventures
of hers that you have just been reading about; and when she had
finished, her sister kissed her, and said, 'It WAS a curious dream,
dear, certainly: but now run in to your tea; it's getting late.' So
Alice got up and ran off, thinking while she ran, as well she might,
what a wonderful dream it had been.
But her sister sat still just as she left her, leaning her head on her
hand, watching the setting sun, and thinking of little Alice and all her
wonderful Adventures, till she too began dreaming after a fashion, and
this was her dream:--
First, she dreamed of little Alice herself, and once again the tiny
hands were clasped upon her knee, and the bright eager eyes were looking
up into hers--she could hear the very tones of her voice, and see that
queer little toss of her head to keep back the wandering hair that
WOULD always get into her eyes--and still as she listened, or seemed to
listen, the whole place around her became alive with the strange creatures
of her little sister's dream.
The long grass rustled at her feet as the White Rabbit hurried by--the
frightened Mouse splashed his way through the neighbouring pool--she
could hear the rattle of the teacups as the March Hare and his friends
shared their never-ending meal, and the shrill voice of the Queen
ordering off her unfortunate guests to execution--once more the pig-baby
was sneezing on the Duchess's knee, while plates and dishes crashed
around it--once more the shriek of the Gryphon, the squeaking of the
Lizard's slate-pencil, and the choking of the suppressed guinea-pigs,
filled the air, mixed up with the distant sobs of the miserable Mock
Turtle.
So she sat on, with closed eyes, and half believed herself in
Wonderland, though she knew she had but to open them again, and all
would change to dull reality--the grass would be only rustling in the
wind, and the pool rippling to the waving of the reeds--the rattling
teacups would change to tinkling sheep-bells, and the Queen's shrill
cries to the voice of the shepherd boy--and the sneeze of the baby, the
shriek of the Gryphon, and all the other queer noises, would change (she
knew) to the confused clamour of the busy farm-yard--while the lowing
of the cattle in the distance would take the place of the Mock Turtle's
heavy sobs.
Lastly, she pictured to herself how this same little sister of hers
would, in the after-time, be herself a grown woman; and how she would
keep, through all her riper years, the simple and loving heart of her
childhood: and how she would gather about her other little children, and
make THEIR eyes bright and eager with many a strange tale, perhaps even
with the dream of Wonderland of long ago: and how she would feel with
all their simple sorrows, and find a pleasure in all their simple joys,
remembering her own child-life, and the happy summer days.
THE END
================================================
FILE: packages/interface-ipfs-core/test/fixtures/hidden-files-folder/files/hello.txt
================================================
Hello
================================================
FILE: packages/interface-ipfs-core/test/fixtures/hidden-files-folder/files/ipfs.txt
================================================
IPFS
================================================
FILE: packages/interface-ipfs-core/test/fixtures/hidden-files-folder/hello-link
================================================
Hello
================================================
FILE: packages/interface-ipfs-core/test/fixtures/hidden-files-folder/holmes.txt
================================================
Project Gutenberg's The Adventures of Sherlock Holmes, by Arthur Conan Doyle
This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever. You may copy it, give it away or
re-use it under the terms of the Project Gutenberg License included
with this eBook or online at www.gutenberg.net
Title: The Adventures of Sherlock Holmes
Author: Arthur Conan Doyle
Posting Date: April 18, 2011 [EBook #1661]
First Posted: November 29, 2002
Language: English
*** START OF THIS PROJECT GUTENBERG EBOOK THE ADVENTURES OF SHERLOCK HOLMES ***
Produced by an anonymous Project Gutenberg volunteer and Jose Menendez
THE ADVENTURES OF SHERLOCK HOLMES
by
SIR ARTHUR CONAN DOYLE
I. A Scandal in Bohemia
II. The Red-headed League
III. A Case of Identity
IV. The Boscombe Valley Mystery
V. The Five Orange Pips
VI. The Man with the Twisted Lip
VII. The Adventure of the Blue Carbuncle
VIII. The Adventure of the Speckled Band
IX. The Adventure of the Engineer's Thumb
X. The Adventure of the Noble Bachelor
XI. The Adventure of the Beryl Coronet
XII. The Adventure of the Copper Beeches
ADVENTURE I. A SCANDAL IN BOHEMIA
I.
To Sherlock Holmes she is always THE woman. I have seldom heard
him mention her under any other name. In his eyes she eclipses
and predominates the whole of her sex. It was not that he felt
any emotion akin to love for Irene Adler. All emotions, and that
one particularly, were abhorrent to his cold, precise but
admirably balanced mind. He was, I take it, the most perfect
reasoning and observing machine that the world has seen, but as a
lover he would have placed himself in a false position. He never
spoke of the softer passions, save with a gibe and a sneer. They
were admirable things for the observer--excellent for drawing the
veil from men's motives and actions. But for the trained reasoner
to admit such intrusions into his own delicate and finely
adjusted temperament was to introduce a distracting factor which
might throw a doubt upon all his mental results. Grit in a
sensitive instrument, or a crack in one of his own high-power
lenses, would not be more disturbing than a strong emotion in a
nature such as his. And yet there was but one woman to him, and
that woman was the late Irene Adler, of dubious and questionable
memory.
I had seen little of Holmes lately. My marriage had drifted us
away from each other. My own complete happiness, and the
home-centred interests which rise up around the man who first
finds himself master of his own establishment, were sufficient to
absorb all my attention, while Holmes, who loathed every form of
society with his whole Bohemian soul, remained in our lodgings in
Baker Street, buried among his old books, and alternating from
week to week between cocaine and ambition, the drowsiness of the
drug, and the fierce energy of his own keen nature. He was still,
as ever, deeply attracted by the study of crime, and occupied his
immense faculties and extraordinary powers of observation in
following out those clues, and clearing up those mysteries which
had been abandoned as hopeless by the official police. From time
to time I heard some vague account of his doings: of his summons
to Odessa in the case of the Trepoff murder, of his clearing up
of the singular tragedy of the Atkinson brothers at Trincomalee,
and finally of the mission which he had accomplished so
delicately and successfully for the reigning family of Holland.
Beyond these signs of his activity, however, which I merely
shared with all the readers of the daily press, I knew little of
my former friend and companion.
One night--it was on the twentieth of March, 1888--I was
returning from a journey to a patient (for I had now returned to
civil practice), when my way led me through Baker Street. As I
passed the well-remembered door, which must always be associated
in my mind with my wooing, and with the dark incidents of the
Study in Scarlet, I was seized with a keen desire to see Holmes
again, and to know how he was employing his extraordinary powers.
His rooms were brilliantly lit, and, even as I looked up, I saw
his tall, spare figure pass twice in a dark silhouette against
the blind. He was pacing the room swiftly, eagerly, with his head
sunk upon his chest and his hands clasped behind him. To me, who
knew his every mood and habit, his attitude and manner told their
own story. He was at work again. He had risen out of his
drug-created dreams and was hot upon the scent of some new
problem. I rang the bell and was shown up to the chamber which
had formerly been in part my own.
His manner was not effusive. It seldom was; but he was glad, I
think, to see me. With hardly a word spoken, but with a kindly
eye, he waved me to an armchair, threw across his case of cigars,
and indicated a spirit case and a gasogene in the corner. Then he
stood before the fire and looked me over in his singular
introspective fashion.
"Wedlock suits you," he remarked. "I think, Watson, that you have
put on seven and a half pounds since I saw you."
"Seven!" I answered.
"Indeed, I should have thought a little more. Just a trifle more,
I fancy, Watson. And in practice again, I observe. You did not
tell me that you intended to go into harness."
"Then, how do you know?"
"I see it, I deduce it. How do I know that you have been getting
yourself very wet lately, and that you have a most clumsy and
careless servant girl?"
"My dear Holmes," said I, "this is too much. You would certainly
have been burned, had you lived a few centuries ago. It is true
that I had a country walk on Thursday and came home in a dreadful
mess, but as I have changed my clothes I can't imagine how you
deduce it. As to Mary Jane, she is incorrigible, and my wife has
given her notice, but there, again, I fail to see how you work it
out."
He chuckled to himself and rubbed his long, nervous hands
together.
"It is simplicity itself," said he; "my eyes tell me that on the
inside of your left shoe, just where the firelight strikes it,
the leather is scored by six almost parallel cuts. Obviously they
have been caused by someone who has very carelessly scraped round
the edges of the sole in order to remove crusted mud from it.
Hence, you see, my double deduction that you had been out in vile
weather, and that you had a particularly malignant boot-slitting
specimen of the London slavey. As to your practice, if a
gentleman walks into my rooms smelling of iodoform, with a black
mark of nitrate of silver upon his right forefinger, and a bulge
on the right side of his top-hat to show where he has secreted
his stethoscope, I must be dull, indeed, if I do not pronounce
him to be an active member of the medical profession."
I could not help laughing at the ease with which he explained his
process of deduction. "When I hear you give your reasons," I
remarked, "the thing always appears to me to be so ridiculously
simple that I could easily do it myself, though at each
successive instance of your reasoning I am baffled until you
explain your process. And yet I believe that my eyes are as good
as yours."
"Quite so," he answered, lighting a cigarette, and throwing
himself down into an armchair. "You see, but you do not observe.
The distinction is clear. For example, you have frequently seen
the steps which lead up from the hall to this room."
"Frequently."
"How often?"
"Well, some hundreds of times."
"Then how many are there?"
"How many? I don't know."
"Quite so! You have not observed. And yet you have seen. That is
just my point. Now, I know that there are seventeen steps,
because I have both seen and observed. By-the-way, since you are
interested in these little problems, and since you are good
enough to chronicle one or two of my trifling experiences, you
may be interested in this." He threw over a sheet of thick,
pink-tinted note-paper which had been lying open upon the table.
"It came by the last post," said he. "Read it aloud."
The note was undated, and without either signature or address.
"There will call upon you to-night, at a quarter to eight
o'clock," it said, "a gentleman who desires to consult you upon a
matter of the very deepest moment. Your recent services to one of
the royal houses of Europe have shown that you are one who may
safely be trusted with matters which are of an importance which
can hardly be exaggerated. This account of you we have from all
quarters received. Be in your chamber then at that hour, and do
not take it amiss if your visitor wear a mask."
"This is indeed a mystery," I remarked. "What do you imagine that
it means?"
"I have no data yet. It is a capital mistake to theorize before
one has data. Insensibly one begins to twist facts to suit
theories, instead of theories to suit facts. But the note itself.
What do you deduce from it?"
I carefully examined the writing, and the paper upon which it was
written.
"The man who wrote it was presumably well to do," I remarked,
endeavouring to imitate my companion's processes. "Such paper
could not be bought under half a crown a packet. It is peculiarly
strong and stiff."
"Peculiar--that is the very word," said Holmes. "It is not an
English paper at all. Hold it up to the light."
I did so, and saw a large "E" with a small "g," a "P," and a
large "G" with a small "t" woven into the texture of the paper.
"What do you make of that?" asked Holmes.
"The name of the maker, no doubt; or his monogram, rather."
"Not at all. The 'G' with the small 't' stands for
'Gesellschaft,' which is the German for 'Company.' It is a
customary contraction like our 'Co.' 'P,' of course, stands for
'Papier.' Now for the 'Eg.' Let us glance at our Continental
Gazetteer." He took down a heavy brown volume from his shelves.
"Eglow, Eglonitz--here we are, Egria. It is in a German-speaking
country--in Bohemia, not far from Carlsbad. 'Remarkable as being
the scene of the death of Wallenstein, and for its numerous
glass-factories and paper-mills.' Ha, ha, my boy, what do you
make of that?" His eyes sparkled, and he sent up a great blue
triumphant cloud from his cigarette.
"The paper was made in Bohemia," I said.
"Precisely. And the man who wrote the note is a German. Do you
note the peculiar construction of the sentence--'This account of
you we have from all quarters received.' A Frenchman or Russian
could not have written that. It is the German who is so
uncourteous to his verbs. It only remains, therefore, to discover
what is wanted by this German who writes upon Bohemian paper and
prefers wearing a mask to showing his face. And here he comes, if
I am not mistaken, to resolve all our doubts."
As he spoke there was the sharp sound of horses' hoofs and
grating wheels against the curb, followed by a sharp pull at the
bell. Holmes whistled.
"A pair, by the sound," said he. "Yes," he continued, glancing
out of the window. "A nice little brougham and a pair of
beauties. A hundred and fifty guineas apiece. There's money in
this case, Watson, if there is nothing else."
"I think that I had better go, Holmes."
"Not a bit, Doctor. Stay where you are. I am lost without my
Boswell. And this promises to be interesting. It would be a pity
to miss it."
"But your client--"
"Never mind him. I may want your help, and so may he. Here he
comes. Sit down in that armchair, Doctor, and give us your best
attention."
A slow and heavy step, which had been heard upon the stairs and
in the passage, paused immediately outside the door. Then there
was a loud and authoritative tap.
"Come in!" said Holmes.
A man entered who could hardly have been less than six feet six
inches in height, with the chest and limbs of a Hercules. His
dress was rich with a richness which would, in England, be looked
upon as akin to bad taste. Heavy bands of astrakhan were slashed
across the sleeves and fronts of his double-breasted coat, while
the deep blue cloak which was thrown over his shoulders was lined
with flame-coloured silk and secured at the neck with a brooch
which consisted of a single flaming beryl. Boots which extended
halfway up his calves, and which were trimmed at the tops with
rich brown fur, completed the impression of barbaric opulence
which was suggested by his whole appearance. He carried a
broad-brimmed hat in his hand, while he wore across the upper
part of his face, extending down past the cheekbones, a black
vizard mask, which he had apparently adjusted that very moment,
for his hand was still raised to it as he entered. From the lower
part of the face he appeared to be a man of strong character,
with a thick, hanging lip, and a long, straight chin suggestive
of resolution pushed to the length of obstinacy.
"You had my note?" he asked with a deep harsh voice and a
strongly marked German accent. "I told you that I would call." He
looked from one to the other of us, as if uncertain which to
address.
"Pray take a seat," said Holmes. "This is my friend and
colleague, Dr. Watson, who is occasionally good enough to help me
in my cases. Whom have I the honour to address?"
"You may address me as the Count Von Kramm, a Bohemian nobleman.
I understand that this gentleman, your friend, is a man of honour
and discretion, whom I may trust with a matter of the most
extreme importance. If not, I should much prefer to communicate
with you alone."
I rose to go, but Holmes caught me by the wrist and pushed me
back into my chair. "It is both, or none," said he. "You may say
before this gentleman anything which you may say to me."
The Count shrugged his broad shoulders. "Then I must begin," said
he, "by binding you both to absolute secrecy for two years; at
the end of that time the matter will be of no importance. At
present it is not too much to say that it is of such weight it
may have an influence upon European history."
"I promise," said Holmes.
"And I."
"You will excuse this mask," continued our strange visitor. "The
august person who employs me wishes his agent to be unknown to
you, and I may confess at once that the title by which I have
just called myself is not exactly my own."
"I was aware of it," said Holmes dryly.
"The circumstances are of great delicacy, and every precaution
has to be taken to quench what might grow to be an immense
scandal and seriously compromise one of the reigning families of
Europe. To speak plainly, the matter implicates the great House
of Ormstein, hereditary kings of Bohemia."
"I was also aware of that," murmured Holmes, settling himself
down in his armchair and closing his eyes.
Our visitor glanced with some apparent surprise at the languid,
lounging figure of the man who had been no doubt depicted to him
as the most incisive reasoner and most energetic agent in Europe.
Holmes slowly reopened his eyes and looked impatiently at his
gigantic client.
"If your Majesty would condescend to state your case," he
remarked, "I should be better able to advise you."
The man sprang from his chair and paced up and down the room in
uncontrollable agitation. Then, with a gesture of desperation, he
tore the mask from his face and hurled it upon the ground. "You
are right," he cried; "I am the King. Why should I attempt to
conceal it?"
"Why, indeed?" murmured Holmes. "Your Majesty had not spoken
before I was aware that I was addressing Wilhelm Gottsreich
Sigismond von Ormstein, Grand Duke of Cassel-Felstein, and
hereditary King of Bohemia."
"But you can understand," said our strange visitor, sitting down
once more and passing his hand over his high white forehead, "you
can understand that I am not accustomed to doing such business in
my own person. Yet the matter was so delicate that I could not
confide it to an agent without putting myself in his power. I
have come incognito from Prague for the purpose of consulting
you."
"Then, pray consult," said Holmes, shutting his eyes once more.
"The facts are briefly these: Some five years ago, during a
lengthy visit to Warsaw, I made the acquaintance of the well-known
adventuress, Irene Adler. The name is no doubt familiar to you."
"Kindly look her up in my index, Doctor," murmured Holmes without
opening his eyes. For many years he had adopted a system of
docketing all paragraphs concerning men and things, so that it
was difficult to name a subject or a person on which he could not
at once furnish information. In this case I found her biography
sandwiched in between that of a Hebrew rabbi and that of a
staff-commander who had written a monograph upon the deep-sea
fishes.
"Let me see!" said Holmes. "Hum! Born in New Jersey in the year
1858. Contralto--hum! La Scala, hum! Prima donna Imperial Opera
of Warsaw--yes! Retired from operatic stage--ha! Living in
London--quite so! Your Majesty, as I understand, became entangled
with this young person, wrote her some compromising letters, and
is now desirous of getting those letters back."
"Precisely so. But how--"
"Was there a secret marriage?"
"None."
"No legal papers or certificates?"
"None."
"Then I fail to follow your Majesty. If this young person should
produce her letters for blackmailing or other purposes, how is
she to prove their authenticity?"
"There is the writing."
"Pooh, pooh! Forgery."
"My private note-paper."
"Stolen."
"My own seal."
"Imitated."
"My photograph."
"Bought."
"We were both in the photograph."
"Oh, dear! That is very bad! Your Majesty has indeed committed an
indiscretion."
"I was mad--insane."
"You have compromised yourself seriously."
"I was only Crown Prince then. I was young. I am but thirty now."
"It must be recovered."
"We have tried and failed."
"Your Majesty must pay. It must be bought."
"She will not sell."
"Stolen, then."
"Five attempts have been made. Twice burglars in my pay ransacked
her house. Once we diverted her luggage when she travelled. Twice
she has been waylaid. There has been no result."
"No sign of it?"
"Absolutely none."
Holmes laughed. "It is quite a pretty little problem," said he.
"But a very serious one to me," returned the King reproachfully.
"Very, indeed. And what does she propose to do with the
photograph?"
"To ruin me."
"But how?"
"I am about to be married."
"So I have heard."
"To Clotilde Lothman von Saxe-Meningen, second daughter of the
King of Scandinavia. You may know the strict principles of her
family. She is herself the very soul of delicacy. A shadow of a
doubt as to my conduct would bring the matter to an end."
"And Irene Adler?"
"Threatens to send them the photograph. And she will do it. I
know that she will do it. You do not know her, but she has a soul
of steel. She has the face of the most beautiful of women, and
the mind of the most resolute of men. Rather than I should marry
another woman, there are no lengths to which she would not
go--none."
"You are sure that she has not sent it yet?"
"I am sure."
"And why?"
"Because she has said that she would send it on the day when the
betrothal was publicly proclaimed. That will be next Monday."
"Oh, then we have three days yet," said Holmes with a yawn. "That
is very fortunate, as I have one or two matters of importance to
look into just at present. Your Majesty will, of course, stay in
London for the present?"
"Certainly. You will find me at the Langham under the name of the
Count Von Kramm."
"Then I shall drop you a line to let you know how we progress."
"Pray do so. I shall be all anxiety."
"Then, as to money?"
"You have carte blanche."
"Absolutely?"
"I tell you that I would give one of the provinces of my kingdom
to have that photograph."
"And for present expenses?"
The King took a heavy chamois leather bag from under his cloak
and laid it on the table.
"There are three hundred pounds in gold and seven hundred in
notes," he said.
Holmes scribbled a receipt upon a sheet of his note-book and
handed it to him.
"And Mademoiselle's address?" he asked.
"Is Briony Lodge, Serpentine Avenue, St. John's Wood."
Holmes took a note of it. "One other question," said he. "Was the
photograph a cabinet?"
"It was."
"Then, good-night, your Majesty, and I trust that we shall soon
have some good news for you. And good-night, Watson," he added,
as the wheels of the royal brougham rolled down the street. "If
you will be good enough to call to-morrow afternoon at three
o'clock I should like to chat this little matter over with you."
II.
At three o'clock precisely I was at Baker Street, but Holmes had
not yet returned. The landlady informed me that he had left the
house shortly after eight o'clock in the morning. I sat down
beside the fire, however, with the intention of awaiting him,
however long he might be. I was already deeply interested in his
inquiry, for, though it was surrounded by none of the grim and
strange features which were associated with the two crimes which
I have already recorded, still, the nature of the case and the
exalted station of his client gave it a character of its own.
Indeed, apart from the nature of the investigation which my
friend had on hand, there was something in his masterly grasp of
a situation, and his keen, incisive reasoning, which made it a
pleasure to me to study his system of work, and to follow the
quick, subtle methods by which he disentangled the most
inextricable mysteries. So accustomed was I to his invariable
success that the very possibility of his failing had ceased to
enter into my head.
It was close upon four before the door opened, and a
drunken-looking groom, ill-kempt and side-whiskered, with an
inflamed face and disreputable clothes, walked into the room.
Accustomed as I was to my friend's amazing powers in the use of
disguises, I had to look three times before I was certain that it
was indeed he. With a nod he vanished into the bedroom, whence he
emerged in five minutes tweed-suited and respectable, as of old.
Putting his hands into his pockets, he stretched out his legs in
front of the fire and laughed heartily for some minutes.
"Well, really!" he cried, and then he choked and laughed again
until he was obliged to lie back, limp and helpless, in the
chair.
"What is it?"
"It's quite too funny. I am sure you could never guess how I
employed my morning, or what I ended by doing."
"I can't imagine. I suppose that you have been watching the
habits, and perhaps the house, of Miss Irene Adler."
"Quite so; but the sequel was rather unusual. I will tell you,
however. I left the house a little after eight o'clock this
morning in the character of a groom out of work. There is a
wonderful sympathy and freemasonry among horsey men. Be one of
them, and you will know all that there is to know. I soon found
Briony Lodge. It is a bijou villa, with a garden at the back, but
built out in front right up to the road, two stories. Chubb lock
to the door. Large sitting-room on the right side, well
furnished, with long windows almost to the floor, and those
preposterous English window fasteners which a child could open.
Behind there was nothing remarkable, save that the passage window
could be reached from the top of the coach-house. I walked round
it and examined it closely from every point of view, but without
noting anything else of interest.
"I then lounged down the street and found, as I expected, that
there was a mews in a lane which runs down by one wall of the
garden. I lent the ostlers a hand in rubbing down their horses,
and received in exchange twopence, a glass of half and half, two
fills of shag tobacco, and as much information as I could desire
about Miss Adler, to say nothing of half a dozen other people in
the neighbourhood in whom I was not in the least interested, but
whose biographies I was compelled to listen to."
"And what of Irene Adler?" I asked.
"Oh, she has turned all the men's heads down in that part. She is
the daintiest thing under a bonnet on this planet. So say the
Serpentine-mews, to a man. She lives quietly, sings at concerts,
drives out at five every day, and returns at seven sharp for
dinner. Seldom goes out at other times, except when she sings.
Has only one male visitor, but a good deal of him. He is dark,
handsome, and dashing, never calls less than once a day, and
often twice. He is a Mr. Godfrey Norton, of the Inner Temple. See
the advantages of a cabman as a confidant. They had driven him
home a dozen times from Serpentine-mews, and knew all about him.
When I had listened to all they had to tell, I began to walk up
and down near Briony Lodge once more, and to think over my plan
of campaign.
"This Godfrey Norton was evidently an important factor in the
matter. He was a lawyer. That sounded ominous. What was the
relation between them, and what the object of his repeated
visits? Was she his client, his friend, or his mistress? If the
former, she had probably transferred the photograph to his
keeping. If the latter, it was less likely. On the issue of this
question depended whether I should continue my work at Briony
Lodge, or turn my attention to the gentleman's chambers in the
Temple. It was a delicate point, and it widened the field of my
inquiry. I fear that I bore you with these details, but I have to
let you see my little difficulties, if you are to understand the
situation."
"I am following you closely," I answered.
"I was still balancing the matter in my mind when a hansom cab
drove up to Briony Lodge, and a gentleman sprang out. He was a
remarkably handsome man, dark, aquiline, and moustached--evidently
the man of whom I had heard. He appeared to be in a
great hurry, shouted to the cabman to wait, and brushed past the
maid who opened the door with the air of a man who was thoroughly
at home.
"He was in the house about half an hour, and I could catch
glimpses of him in the windows of the sitting-room, pacing up and
down, talking excitedly, and waving his arms. Of her I could see
nothing. Presently he emerged, looking even more flurried than
before. As he stepped up to the cab, he pulled a gold watch from
his pocket and looked at it earnestly, 'Drive like the devil,' he
shouted, 'first to Gross & Hankey's in Regent Street, and then to
the Church of St. Monica in the Edgeware Road. Half a guinea if
you do it in twenty minutes!'
"Away they went, and I was just wondering whether I should not do
well to follow them when up the lane came a neat little landau,
the coachman with his coat only half-buttoned, and his tie under
his ear, while all the tags of his harness were sticking out of
the buckles. It hadn't pulled up before she shot out of the hall
door and into it. I only caught a glimpse of her at the moment,
but she was a lovely woman, with a face that a man might die for.
"'The Church of St. Monica, John,' she cried, 'and half a
sovereign if you reach it in twenty minutes.'
"This was quite too good to lose, Watson. I was just balancing
whether I should run for it, or whether I should perch behind her
landau when a cab came through the street. The driver looked
twice at such a shabby fare, but I jumped in before he could
object. 'The Church of St. Monica,' said I, 'and half a sovereign
if you reach it in twenty minutes.' It was twenty-five minutes to
twelve, and of course it was clear enough what was in the wind.
"My cabby drove fast. I don't think I ever drove faster, but the
others were there before us. The cab and the landau with their
steaming horses were in front of the door when I arrived. I paid
the man and hurried into the church. There was not a soul there
save the two whom I had followed and a surpliced clergyman, who
seemed to be expostulating with them. They were all three
standing in a knot in front of the altar. I lounged up the side
aisle like any other idler who has dropped into a church.
Suddenly, to my surprise, the three at the altar faced round to
me, and Godfrey Norton came running as hard as he could towards
me.
"'Thank God,' he cried. 'You'll do. Come! Come!'
"'What then?' I asked.
"'Come, man, come, only three minutes, or it won't be legal.'
"I was half-dragged up to the altar, and before I knew where I was
I found myself mumbling responses which were whispered in my ear,
and vouching for things of which I knew nothing, and generally
assisting in the secure tying up of Irene Adler, spinster, to
Godfrey Norton, bachelor. It was all done in an instant, and
there was the gentleman thanking me on the one side and the lady
on the other, while the clergyman beamed on me in front. It was
the most preposterous position in which I ever found myself in my
life, and it was the thought of it that started me laughing just
now. It seems that there had been some informality about their
license, that the clergyman absolutely refused to marry them
without a witness of some sort, and that my lucky appearance
saved the bridegroom from having to sally out into the streets in
search of a best man. The bride gave me a sovereign, and I mean
to wear it on my watch-chain in memory of the occasion."
"This is a very unexpected turn of affairs," said I; "and what
then?"
"Well, I found my plans very seriously menaced. It looked as if
the pair might take an immediate departure, and so necessitate
very prompt and energetic measures on my part. At the church
door, however, they separated, he driving back to the Temple, and
she to her own house. 'I shall drive out in the park at five as
usual,' she said as she left him. I heard no more. They drove
away in different directions, and I went off to make my own
arrangements."
"Which are?"
"Some cold beef and a glass of beer," he answered, ringing the
bell. "I have been too busy to think of food, and I am likely to
be busier still this evening. By the way, Doctor, I shall want
your co-operation."
"I shall be delighted."
"You don't mind breaking the law?"
"Not in the least."
"Nor running a chance of arrest?"
"Not in a good cause."
"Oh, the cause is excellent!"
"Then I am your man."
"I was sure that I might rely on you."
"But what is it you wish?"
"When Mrs. Turner has brought in the tray I will make it clear to
you. Now," he said as he turned hungrily on the simple fare that
our landlady had provided, "I must discuss it while I eat, for I
have not much time. It is nearly five now. In two hours we must
be on the scene of action. Miss Irene, or Madame, rather, returns
from her drive at seven. We must be at Briony Lodge to meet her."
"And what then?"
"You must leave that to me. I have already arranged what is to
occur. There is only one point on which I must insist. You must
not interfere, come what may. You understand?"
"I am to be neutral?"
"To do nothing whatever. There will probably be some small
unpleasantness. Do not join in it. It will end in my being
conveyed into the house. Four or five minutes afterwards the
sitting-room window will open. You are to station yourself close
to that open window."
"Yes."
"You are to watch me, for I will be visible to you."
"Yes."
"And when I raise my hand--so--you will throw into the room what
I give you to throw, and will, at the same time, raise the cry of
fire. You quite follow me?"
"Entirely."
"It is nothing very formidable," he said, taking a long cigar-shaped
roll from his pocket. "It is an ordinary plumber's smoke-rocket,
fitted with a cap at either end to make it self-lighting.
Your task is confined to that. When you raise your cry of fire,
it will be taken up by quite a number of people. You may then
walk to the end of the street, and I will rejoin you in ten
minutes. I hope that I have made myself clear?"
"I am to remain neutral, to get near the window, to watch you,
and at the signal to throw in this object, then to raise the cry
of fire, and to wait you at the corner of the street."
"Precisely."
"Then you may entirely rely on me."
"That is excellent. I think, perhaps, it is almost time that I
prepare for the new role I have to play."
He disappeared into his bedroom and returned in a few minutes in
the character of an amiable and simple-minded Nonconformist
clergyman. His broad black hat, his baggy trousers, his white
tie, his sympathetic smile, and general look of peering and
benevolent curiosity were such as Mr. John Hare alone could have
equalled. It was not merely that Holmes changed his costume. His
expression, his manner, his very soul seemed to vary with every
fresh part that he assumed. The stage lost a fine actor, even as
science lost an acute reasoner, when he became a specialist in
crime.
It was a quarter past six when we left Baker Street, and it still
wanted ten minutes to the hour when we found ourselves in
Serpentine Avenue. It was already dusk, and the lamps were just
being lighted as we paced up and down in front of Briony Lodge,
waiting for the coming of its occupant. The house was just such
as I had pictured it from Sherlock Holmes' succinct description,
but the locality appeared to be less private than I expected. On
the contrary, for a small street in a quiet neighbourhood, it was
remarkably animated. There was a group of shabbily dressed men
smoking and laughing in a corner, a scissors-grinder with his
wheel, two guardsmen who were flirting with a nurse-girl, and
several well-dressed young men who were lounging up and down with
cigars in their mouths.
"You see," remarked Holmes, as we paced to and fro in front of
the house, "this marriage rather simplifies matters. The
photograph becomes a double-edged weapon now. The chances are
that she would be as averse to its being seen by Mr. Godfrey
Norton, as our client is to its coming to the eyes of his
princess. Now the question is, Where are we to find the
photograph?"
"Where, indeed?"
"It is most unlikely that she carries it about with her. It is
cabinet size. Too large for easy concealment about a woman's
dress. She knows that the King is capable of having her waylaid
and searched. Two attempts of the sort have already been made. We
may take it, then, that she does not carry it about with her."
"Where, then?"
"Her banker or her lawyer. There is that double possibility. But
I am inclined to think neither. Women are naturally secretive,
and they like to do their own secreting. Why should she hand it
over to anyone else? She could trust her own guardianship, but
she could not tell what indirect or political influence might be
brought to bear upon a business man. Besides, remember that she
had resolved to use it within a few days. It must be where she
can lay her hands upon it. It must be in her own house."
"But it has twice been burgled."
"Pshaw! They did not know how to look."
"But how will you look?"
"I will not look."
"What then?"
"I will get her to show me."
"But she will refuse."
"She will not be able to. But I hear the rumble of wheels. It is
her carriage. Now carry out my orders to the letter."
As he spoke the gleam of the side-lights of a carriage came round
the curve of the avenue. It was a smart little landau which
rattled up to the door of Briony Lodge. As it pulled up, one of
the loafing men at the corner dashed forward to open the door in
the hope of earning a copper, but was elbowed away by another
loafer, who had rushed up with the same intention. A fierce
quarrel broke out, which was increased by the two guardsmen, who
took sides with one of the loungers, and by the scissors-grinder,
who was equally hot upon the other side. A blow was struck, and
in an instant the lady, who had stepped from her carriage, was
the centre of a little knot of flushed and struggling men, who
struck savagely at each other with their fists and sticks. Holmes
dashed into the crowd to protect the lady; but just as he reached
her he gave a cry and dropped to the ground, with the blood
running freely down his face. At his fall the guardsmen took to
their heels in one direction and the loungers in the other, while
a number of better-dressed people, who had watched the scuffle
without taking part in it, crowded in to help the lady and to
attend to the injured man. Irene Adler, as I will still call her,
had hurried up the steps; but she stood at the top with her
superb figure outlined against the lights of the hall, looking
back into the street.
"Is the poor gentleman much hurt?" she asked.
"He is dead," cried several voices.
"No, no, there's life in him!" shouted another. "But he'll be
gone before you can get him to hospital."
"He's a brave fellow," said a woman. "They would have had the
lady's purse and watch if it hadn't been for him. They were a
gang, and a rough one, too. Ah, he's breathing now."
"He can't lie in the street. May we bring him in, marm?"
"Surely. Bring him into the sitting-room. There is a comfortable
sofa. This way, please!"
Slowly and solemnly he was borne into Briony Lodge and laid out
in the principal room, while I still observed the proceedings
from my post by the window. The lamps had been lit, but the
blinds had not been drawn, so that I could see Holmes as he lay
upon the couch. I do not know whether he was seized with
compunction at that moment for the part he was playing, but I
know that I never felt more heartily ashamed of myself in my life
than when I saw the beautiful creature against whom I was
conspiring, or the grace and kindliness with which she waited
upon the injured man. And yet it would be the blackest treachery
to Holmes to draw back now from the part which he had intrusted
to me. I hardened my heart, and took the smoke-rocket from under
my ulster. After all, I thought, we are not injuring her. We are
but preventing her from injuring another.
Holmes had sat up upon the couch, and I saw him motion like a man
who is in need of air. A maid rushed across and threw open the
window. At the same instant I saw him raise his hand and at the
signal I tossed my rocket into the room with a cry of "Fire!" The
word was no sooner out of my mouth than the whole crowd of
spectators, well dressed and ill--gentlemen, ostlers, and
servant-maids--joined in a general shriek of "Fire!" Thick clouds
of smoke curled through the room and out at the open window. I
caught a glimpse of rushing figures, and a moment later the voice
of Holmes from within assuring them that it was a false alarm.
Slipping through the shouting crowd I made my way to the corner
of the street, and in ten minutes was rejoiced to find my
friend's arm in mine, and to get away from the scene of uproar.
He walked swiftly and in silence for some few minutes until we
had turned down one of the quiet streets which lead towards the
Edgeware Road.
"You did it very nicely, Doctor," he remarked. "Nothing could
have been better. It is all right."
"You have the photograph?"
"I know where it is."
"And how did you find out?"
"She showed me, as I told you she would."
"I am still in the dark."
"I do not wish to make a mystery," said he, laughing. "The matter
was perfectly simple. You, of course, saw that everyone in the
street was an accomplice. They were all engaged for the evening."
"I guessed as much."
"Then, when the row broke out, I had a little moist red paint in
the palm of my hand. I rushed forward, fell down, clapped my hand
to my face, and became a piteous spectacle. It is an old trick."
"That also I could fathom."
"Then they carried me in. She was bound to have me in. What else
could she do? And into her sitting-room, which was the very room
which I suspected. It lay between that and her bedroom, and I was
determined to see which. They laid me on a couch, I motioned for
air, they were compelled to open the window, and you had your
chance."
"How did that help you?"
"It was all-important. When a woman thinks that her house is on
fire, her instinct is at once to rush to the thing which she
values most. It is a perfectly overpowering impulse, and I have
more than once taken advantage of it. In the case of the
Darlington substitution scandal it was of use to me, and also in
the Arnsworth Castle business. A married woman grabs at her baby;
an unmarried one reaches for her jewel-box. Now it was clear to
me that our lady of to-day had nothing in the house more precious
to her than what we are in quest of. She would rush to secure it.
The alarm of fire was admirably done. The smoke and shouting were
enough to shake nerves of steel. She responded beautifully. The
photograph is in a recess behind a sliding panel just above the
right bell-pull. She was there in an instant, and I caught a
glimpse of it as she half-drew it out. When I cried out that it
was a false alarm, she replaced it, glanced at the rocket, rushed
from the room, and I have not seen her since. I rose, and, making
my excuses, escaped from the house. I hesitated whether to
attempt to secure the photograph at once; but the coachman had
come in, and as he was watching me narrowly it seemed safer to
wait. A little over-precipitance may ruin all."
"And now?" I asked.
"Our quest is practically finished. I shall call with the King
to-morrow, and with you, if you care to come with us. We will be
shown into the sitting-room to wait for the lady, but it is
probable that when she comes she may find neither us nor the
photograph. It might be a satisfaction to his Majesty to regain
it with his own hands."
"And when will you call?"
"At eight in the morning. She will not be up, so that we shall
have a clear field. Besides, we must be prompt, for this marriage
may mean a complete change in her life and habits. I must wire to
the King without delay."
We had reached Baker Street and had stopped at the door. He was
searching his pockets for the key when someone passing said:
"Good-night, Mister Sherlock Holmes."
There were several people on the pavement at the time, but the
greeting appeared to come from a slim youth in an ulster who had
hurried by.
"I've heard that voice before," said Holmes, staring down the
dimly lit street. "Now, I wonder who the deuce that could have
been."
III.
I slept at Baker Street that night, and we were engaged upon our
toast and coffee in the morning when the King of Bohemia rushed
into the room.
"You have really got it!" he cried, grasping Sherlock Holmes by
either shoulder and looking eagerly into his face.
"Not yet."
"But you have hopes?"
"I have hopes."
"Then, come. I am all impatience to be gone."
"We must have a cab."
"No, my brougham is waiting."
"Then that will simplify matters." We descended and started off
once more for Briony Lodge.
"Irene Adler is married," remarked Holmes.
"Married! When?"
"Yesterday."
"But to whom?"
"To an English lawyer named Norton."
"But she could not love him."
"I am in hopes that she does."
"And why in hopes?"
"Because it would spare your Majesty all fear of future
annoyance. If the lady loves her husband, she does not love your
Majesty. If she does not love your Majesty, there is no reason
why she should interfere with your Majesty's plan."
"It is true. And yet--Well! I wish she had been of my own
station! What a queen she would have made!" He relapsed into a
moody silence, which was not broken until we drew up in
Serpentine Avenue.
The door of Briony Lodge was open, and an elderly woman stood
upon the steps. She watched us with a sardonic eye as we stepped
from the brougham.
"Mr. Sherlock Holmes, I believe?" said she.
"I am Mr. Holmes," answered my companion, looking at her with a
questioning and rather startled gaze.
"Indeed! My mistress told me that you were likely to call. She
left this morning with her husband by the 5:15 train from Charing
Cross for the Continent."
"What!" Sherlock Holmes staggered back, white with chagrin and
surprise. "Do you mean that she has left England?"
"Never to return."
"And the papers?" asked the King hoarsely. "All is lost."
"We shall see." He pushed past the servant and rushed into the
drawing-room, followed by the King and myself. The furniture was
scattered about in every direction, with dismantled shelves and
open drawers, as if the lady had hurriedly ransacked them before
her flight. Holmes rushed at the bell-pull, tore back a small
sliding shutter, and, plunging in his hand, pulled out a
photograph and a letter. The photograph was of Irene Adler
herself in evening dress, the letter was superscribed to
"Sherlock Holmes, Esq. To be left till called for." My friend
tore it open and we all three read it together. It was dated at
midnight of the preceding night and ran in this way:
"MY DEAR MR. SHERLOCK HOLMES,--You really did it very well. You
took me in completely. Until after the alarm of fire, I had not a
suspicion. But then, when I found how I had betrayed myself, I
began to think. I had been warned against you months ago. I had
been told that if the King employed an agent it would certainly
be you. And your address had been given me. Yet, with all this,
you made me reveal what you wanted to know. Even after I became
suspicious, I found it hard to think evil of such a dear, kind
old clergyman. But, you know, I have been trained as an actress
myself. Male costume is nothing new to me. I often take advantage
of the freedom which it gives. I sent John, the coachman, to
watch you, ran up stairs, got into my walking-clothes, as I call
them, and came down just as you departed.
"Well, I followed you to your door, and so made sure that I was
really an object of interest to the celebrated Mr. Sherlock
Holmes. Then I, rather imprudently, wished you good-night, and
started for the Temple to see my husband.
"We both thought the best resource was flight, when pursued by
so formidable an antagonist; so you will find the nest empty when
you call to-morrow. As to the photograph, your client may rest in
peace. I love and am loved by a better man than he. The King may
do what he will without hindrance from one whom he has cruelly
wronged. I keep it only to safeguard myself, and to preserve a
weapon which will always secure me from any steps which he might
take in the future. I leave a photograph which he might care to
possess; and I remain, dear Mr. Sherlock Holmes,
"Very truly yours,
"IRENE NORTON, née ADLER."
"What a woman--oh, what a woman!" cried the King of Bohemia, when
we had all three read this epistle. "Did I not tell you how quick
and resolute she was? Would she not have made an admirable queen?
Is it not a pity that she was not on my level?"
"From what I have seen of the lady she seems indeed to be on a
very different level to your Majesty," said Holmes coldly. "I am
sorry that I have not been able to bring your Majesty's business
to a more successful conclusion."
"On the contrary, my dear sir," cried the King; "nothing could be
more successful. I know that her word is inviolate. The
photograph is now as safe as if it were in the fire."
"I am glad to hear your Majesty say so."
"I am immensely indebted to you. Pray tell me in what way I can
reward you. This ring--" He slipped an emerald snake ring from
his finger and held it out upon the palm of his hand.
"Your Majesty has something which I should value even more
highly," said Holmes.
"You have but to name it."
"This photograph!"
The King stared at him in amazement.
"Irene's photograph!" he cried. "Certainly, if you wish it."
"I thank your Majesty. Then there is no more to be done in the
matter. I have the honour to wish you a very good-morning." He
bowed, and, turning away without observing the hand which the
King had stretched out to him, he set off in my company for his
chambers.
And that was how a great scandal threatened to affect the kingdom
of Bohemia, and how the best plans of Mr. Sherlock Holmes were
beaten by a woman's wit. He used to make merry over the
cleverness of women, but I have not heard him do it of late. And
when he speaks of Irene Adler, or when he refers to her
photograph, it is always under the honourable title of the woman.
ADVENTURE II. THE RED-HEADED LEAGUE
I had called upon my friend, Mr. Sherlock Holmes, one day in the
autumn of last year and found him in deep conversation with a
very stout, florid-faced, elderly gentleman with fiery red hair.
With an apology for my intrusion, I was about to withdraw when
Holmes pulled me abruptly into the room and closed the door
behind me.
"You could not possibly have come at a better time, my dear
Watson," he said cordially.
"I was afraid that you were engaged."
"So I am. Very much so."
"Then I can wait in the next room."
"Not at all. This gentleman, Mr. Wilson, has been my partner and
helper in many of my most successful cases, and I have no
doubt that he will be of the utmost use to me in yours also."
The stout gentleman half rose from his chair and gave a bob of
greeting, with a quick little questioning glance from his small
fat-encircled eyes.
"Try the settee," said Holmes, relapsing into his armchair and
putting his fingertips together, as was his custom when in
judicial moods. "I know, my dear Watson, that you share my love
of all that is bizarre and outside the conventions and humdrum
routine of everyday life. You have shown your relish for it by
the enthusiasm which has prompted you to chronicle, and, if you
will excuse my saying so, somewhat to embellish so many of my own
little adventures."
"Your cases have indeed been of the greatest interest to me," I
observed.
"You will remember that I remarked the other day, just before we
went into the very simple problem presented by Miss Mary
Sutherland, that for strange effects and extraordinary
combinations we must go to life itself, which is always far more
daring than any effort of the imagination."
"A proposition which I took the liberty of doubting."
"You did, Doctor, but none the less you must come round to my
view, for otherwise I shall keep on piling fact upon fact on you
until your reason breaks down under them and acknowledges me to
be right. Now, Mr. Jabez Wilson here has been good enough to call
upon me this morning, and to begin a narrative which promises to
be one of the most singular which I have listened to for some
time. You have heard me remark that the strangest and most unique
things are very often connected not with the larger but with the
smaller crimes, and occasionally, indeed, where there is room for
doubt whether any positive crime has been committed. As far as I
have heard it is impossible for me to say whether the present
case is an instance of crime or not, but the course of events is
certainly among the most singular that I have ever listened to.
Perhaps, Mr. Wilson, you would have the great kindness to
recommence your narrative. I ask you not merely because my friend
Dr. Watson has not heard the opening part but also because the
peculiar nature of the story makes me anxious to have every
possible detail from your lips. As a rule, when I have heard some
slight indication of the course of events, I am able to guide
myself by the thousands of other similar cases which occur to my
memory. In the present instance I am forced to admit that the
facts are, to the best of my belief, unique."
The portly client puffed out his chest with an appearance of some
little pride and pulled a dirty and wrinkled newspaper from the
inside pocket of his greatcoat. As he glanced down the
advertisement column, with his head thrust forward and the paper
flattened out upon his knee, I took a good look at the man and
endeavoured, after the fashion of my companion, to read the
indications which might be presented by his dress or appearance.
I did not gain very much, however, by my inspection. Our visitor
bore every mark of being an average commonplace British
tradesman, obese, pompous, and slow. He wore rather baggy grey
shepherd's check trousers, a not over-clean black frock-coat,
unbuttoned in the front, and a drab waistcoat with a heavy brassy
Albert chain, and a square pierced bit of metal dangling down as
an ornament. A frayed top-hat and a faded brown overcoat with a
wrinkled velvet collar lay upon a chair beside him. Altogether,
look as I would, there was nothing remarkable about the man save
his blazing red head, and the expression of extreme chagrin and
discontent upon his features.
Sherlock Holmes' quick eye took in my occupation, and he shook
his head with a smile as he noticed my questioning glances.
"Beyond the obvious facts that he has at some time done manual
labour, that he takes snuff, that he is a Freemason, that he has
been in China, and that he has done a considerable amount of
writing lately, I can deduce nothing else."
Mr. Jabez Wilson started up in his chair, with his forefinger
upon the paper, but his eyes upon my companion.
"How, in the name of good-fortune, did you know all that, Mr.
Holmes?" he asked. "How did you know, for example, that I did
manual labour. It's as true as gospel, for I began as a ship's
carpenter."
"Your hands, my dear sir. Your right hand is quite a size larger
than your left. You have worked with it, and the muscles are more
developed."
"Well, the snuff, then, and the Freemasonry?"
"I won't insult your intelligence by telling you how I read that,
especially as, rather against the strict rules of your order, you
use an arc-and-compass breastpin."
"Ah, of course, I forgot that. But the writing?"
"What else can be indicated by that right cuff so very shiny for
five inches, and the left one with the smooth patch near the
elbow where you rest it upon the desk?"
"Well, but China?"
"The fish that you have tattooed immediately above your right
wrist could only have been done in China. I have made a small
study of tattoo marks and have even contributed to the literature
of the subject. That trick of staining the fishes' scales of a
delicate pink is quite peculiar to China. When, in addition, I
see a Chinese coin hanging from your watch-chain, the matter
becomes even more simple."
Mr. Jabez Wilson laughed heavily. "Well, I never!" said he. "I
thought at first that you had done something clever, but I see
that there was nothing in it, after all."
"I begin to think, Watson," said Holmes, "that I make a mistake
in explaining. 'Omne ignotum pro magnifico,' you know, and my
poor little reputation, such as it is, will suffer shipwreck if I
am so candid. Can you not find the advertisement, Mr. Wilson?"
"Yes, I have got it now," he answered with his thick red finger
planted halfway down the column. "Here it is. This is what began
it all. You just read it for yourself, sir."
I took the paper from him and read as follows:
"TO THE RED-HEADED LEAGUE: On account of the bequest of the late
Ezekiah Hopkins, of Lebanon, Pennsylvania, U. S. A., there is now
another vacancy open which entitles a member of the League to a
salary of 4 pounds a week for purely nominal services. All
red-headed men who are sound in body and mind and above the age
of twenty-one years, are eligible. Apply in person on Monday, at
eleven o'clock, to Duncan Ross, at the offices of the League, 7
Pope's Court, Fleet Street."
"What on earth does this mean?" I ejaculated after I had twice
read over the extraordinary announcement.
Holmes chuckled and wriggled in his chair, as was his habit when
in high spirits. "It is a little off the beaten track, isn't it?"
said he. "And now, Mr. Wilson, off you go at scratch and tell us
all about yourself, your household, and the effect which this
advertisement had upon your fortunes. You will first make a note,
Doctor, of the paper and the date."
"It is The Morning Chronicle of April 27, 1890. Just two months
ago."
"Very good. Now, Mr. Wilson?"
"Well, it is just as I have been telling you, Mr. Sherlock
Holmes," said Jabez Wilson, mopping his forehead; "I have a small
pawnbroker's business at Coburg Square, near the City. It's not a
very large affair, and of late years it has not done more than
just give me a living. I used to be able to keep two assistants,
but now I only keep one; and I would have a job to pay him but
that he is willing to come for half wages so as to learn the
business."
"What is the name of this obliging youth?" asked Sherlock Holmes.
"His name is Vincent Spaulding, and he's not such a youth,
either. It's hard to say his age. I should not wish a smarter
assistant, Mr. Holmes; and I know very well that he could better
himself and earn twice what I am able to give him. But, after
all, if he is satisfied, why should I put ideas in his head?"
"Why, indeed? You seem most fortunate in having an employé who
comes under the full market price. It is not a common experience
among employers in this age. I don't know that your assistant is
not as remarkable as your advertisement."
"Oh, he has his faults, too," said Mr. Wilson. "Never was such a
fellow for photography. Snapping away with a camera when he ought
to be improving his mind, and then diving down into the cellar
like a rabbit into its hole to develop his pictures. That is his
main fault, but on the whole he's a good worker. There's no vice
in him."
"He is still with you, I presume?"
"Yes, sir. He and a girl of fourteen, who does a bit of simple
cooking and keeps the place clean--that's all I have in the
house, for I am a widower and never had any family. We live very
quietly, sir, the three of us; and we keep a roof over our heads
and pay our debts, if we do nothing more.
"The first thing that put us out was that advertisement.
Spaulding, he came down into the office just this day eight
weeks, with this very paper in his hand, and he says:
"'I wish to the Lord, Mr. Wilson, that I was a red-headed man.'
"'Why that?' I asks.
"'Why,' says he, 'here's another vacancy on the League of the
Red-headed Men. It's worth quite a little fortune to any man who
gets it, and I understand that there are more vacancies than
there are men, so that the trustees are at their wits' end what
to do with the money. If my hair would only change colour, here's
a nice little crib all ready for me to step into.'
"'Why, what is it, then?' I asked. You see, Mr. Holmes, I am a
very stay-at-home man, and as my business came to me instead of
my having to go to it, I was often weeks on end without putting
my foot over the door-mat. In that way I didn't know much of what
was going on outside, and I was always glad of a bit of news.
"'Have you never heard of the League of the Red-headed Men?' he
asked with his eyes open.
"'Never.'
"'Why, I wonder at that, for you are eligible yourself for one
of the vacancies.'
"'And what are they worth?' I asked.
"'Oh, merely a couple of hundred a year, but the work is slight,
and it need not interfere very much with one's other
occupations.'
"Well, you can easily think that that made me prick up my ears,
for the business has not been over-good for some years, and an
extra couple of hundred would have been very handy.
"'Tell me all about it,' said I.
"'Well,' said he, showing me the advertisement, 'you can see for
yourself that the League has a vacancy, and there is the address
where you should apply for particulars. As far as I can make out,
the League was founded by an American millionaire, Ezekiah
Hopkins, who was very peculiar in his ways. He was himself
red-headed, and he had a great sympathy for all red-headed men;
so when he died it was found that he had left his enormous
fortune in the hands of trustees, with instructions to apply the
interest to the providing of easy berths to men whose hair is of
that colour. From all I hear it is splendid pay and very little to
do.'
"'But,' said I, 'there would be millions of red-headed men who
would apply.'
"'Not so many as you might think,' he answered. 'You see it is
really confined to Londoners, and to grown men. This American had
started from London when he was young, and he wanted to do the
old town a good turn. Then, again, I have heard it is no use your
applying if your hair is light red, or dark red, or anything but
real bright, blazing, fiery red. Now, if you cared to apply, Mr.
Wilson, you would just walk in; but perhaps it would hardly be
worth your while to put yourself out of the way for the sake of a
few hundred pounds.'
"Now, it is a fact, gentlemen, as you may see for yourselves,
that my hair is of a very full and rich tint, so that it seemed
to me that if there was to be any competition in the matter I
stood as good a chance as any man that I had ever met. Vincent
Spaulding seemed to know so much about it that I thought he might
prove useful, so I just ordered him to put up the shutters for
the day and to come right away with me. He was very willing to
have a holiday, so we shut the business up and started off for
the address that was given us in the advertisement.
"I never hope to see such a sight as that again, Mr. Holmes. From
north, south, east, and west every man who had a shade of red in
his hair had tramped into the city to answer the advertisement.
Fleet Street was choked with red-headed folk, and Pope's Court
looked like a coster's orange barrow. I should not have thought
there were so many in the whole country as were brought together
by that single advertisement. Every shade of colour they
were--straw, lemon, orange, brick, Irish-setter, liver, clay;
but, as Spaulding said, there were not many who had the real
vivid flame-coloured tint. When I saw how many were waiting, I
would have given it up in despair; but Spaulding would not hear
of it. How he did it I could not imagine, but he pushed and
pulled and butted until he got me through the crowd, and right up
to the steps which led to the office. There was a double stream
upon the stair, some going up in hope, and some coming back
dejected; but we wedged in as well as we could and soon found
ourselves in the office."
"Your experience has been a most entertaining one," remarked
Holmes as his client paused and refreshed his memory with a huge
pinch of snuff. "Pray continue your very interesting statement."
"There was nothing in the office but a couple of wooden chairs
and a deal table, behind which sat a small man with a head that
was even redder than mine. He said a few words to each candidate
as he came up, and then he always managed to find some fault in
them which would disqualify them. Getting a vacancy did not seem
to be such a very easy matter, after all. However, when our turn
came the little man was much more favourable to me than to any of
the others, and he closed the door as we entered, so that he
might have a private word with us.
"'This is Mr. Jabez Wilson,' said my assistant, 'and he is
willing to fill a vacancy in the League.'
"'And he is admirably suited for it,' the other answered. 'He has
every requirement. I cannot recall when I have seen anything so
fine.' He took a step backward, cocked his head on one side, and
gazed at my hair until I felt quite bashful. Then suddenly he
plunged forward, wrung my hand, and congratulated me warmly on my
success.
"'It would be injustice to hesitate,' said he. 'You will,
however, I am sure, excuse me for taking an obvious precaution.'
With that he seized my hair in both his hands, and tugged until I
yelled with the pain. 'There is water in your eyes,' said he as
he released me. 'I perceive that all is as it should be. But we
have to be careful, for we have twice been deceived by wigs and
once by paint. I could tell you tales of cobbler's wax which
would disgust you with human nature.' He stepped over to the
window and shouted through it at the top of his voice that the
vacancy was filled. A groan of disappointment came up from below,
and the folk all trooped away in different directions until there
was not a red-head to be seen except my own and that of the
manager.
"'My name,' said he, 'is Mr. Duncan Ross, and I am myself one of
the pensioners upon the fund left by our noble benefactor. Are
you a married man, Mr. Wilson? Have you a family?'
"I answered that I had not.
"His face fell immediately.
"'Dear me!' he said gravely, 'that is very serious indeed! I am
sorry to hear you say that. The fund was, of course, for the
propagation and spread of the red-heads as well as for their
maintenance. It is exceedingly unfortunate that you should be a
bachelor.'
"My face lengthened at this, Mr. Holmes, for I thought that I was
not to have the vacancy after all; but after thinking it over for
a few minutes he said that it would be all right.
"'In the case of another,' said he, 'the objection might be
fatal, but we must stretch a point in favour of a man with such a
head of hair as yours. When shall you be able to enter upon your
new duties?'
"'Well, it is a little awkward, for I have a business already,'
said I.
"'Oh, never mind about that, Mr. Wilson!' said Vincent Spaulding.
'I should be able to look after that for you.'
"'What would be the hours?' I asked.
"'Ten to two.'
"Now a pawnbroker's business is mostly done of an evening, Mr.
Holmes, especially Thursday and Friday evening, which is just
before pay-day; so it would suit me very well to earn a little in
the mornings. Besides, I knew that my assistant was a good man,
and that he would see to anything that turned up.
"'That would suit me very well,' said I. 'And the pay?'
"'Is 4 pounds a week.'
"'And the work?'
"'Is purely nominal.'
"'What do you call purely nominal?'
"'Well, you have to be in the office, or at least in the
building, the whole time. If you leave, you forfeit your whole
position forever. The will is very clear upon that point. You
don't comply with the conditions if you budge from the office
during that time.'
"'It's only four hours a day, and I should not think of leaving,'
said I.
"'No excuse will avail,' said Mr. Duncan Ross; 'neither sickness
nor business nor anything else. There you must stay, or you lose
your billet.'
"'And the work?'
"'Is to copy out the "Encyclopaedia Britannica." There is the first
volume of it in that press. You must find your own ink, pens, and
blotting-paper, but we provide this table and chair. Will you be
ready to-morrow?'
"'Certainly,' I answered.
"'Then, good-bye, Mr. Jabez Wilson, and let me congratulate you
once more on the important position which you have been fortunate
enough to gain.' He bowed me out of the room and I went home with
my assistant, hardly knowing what to say or do, I was so pleased
at my own good fortune.
"Well, I thought over the matter all day, and by evening I was in
low spirits again; for I had quite persuaded myself that the
whole affair must be some great hoax or fraud, though what its
object might be I could not imagine. It seemed altogether past
belief that anyone could make such a will, or that they would pay
such a sum for doing anything so simple as copying out the
'Encyclopaedia Britannica.' Vincent Spaulding did what he could to
cheer me up, but by bedtime I had reasoned myself out of the
whole thing. However, in the morning I determined to have a look
at it anyhow, so I bought a penny bottle of ink, and with a
quill-pen, and seven sheets of foolscap paper, I started off for
Pope's Court.
"Well, to my surprise and delight, everything was as right as
possible. The table was set out ready for me, and Mr. Duncan Ross
was there to see that I got fairly to work. He started me off
upon the letter A, and then he left me; but he would drop in from
time to time to see that all was right with me. At two o'clock he
bade me good-day, complimented me upon the amount that I had
written, and locked the door of the office after me.
"This went on day after day, Mr. Holmes, and on Saturday the
manager came in and planked down four golden sovereigns for my
week's work. It was the same next week, and the same the week
after. Every morning I was there at ten, and every afternoon I
left at two. By degrees Mr. Duncan Ross took to coming in only
once of a morning, and then, after a time, he did not come in at
all. Still, of course, I never dared to leave the room for an
instant, for I was not sure when he might come, and the billet
was such a good one, and suited me so well, that I would not risk
the loss of it.
"Eight weeks passed away like this, and I had written about
Abbots and Archery and Armour and Architecture and Attica, and
hoped with diligence that I might get on to the B's before very
long. It cost me something in foolscap, and I had pretty nearly
filled a shelf with my writings. And then suddenly the whole
business came to an end."
"To an end?"
"Yes, sir. And no later than this morning. I went to my work as
usual at ten o'clock, but the door was shut and locked, with a
little square of cardboard hammered on to the middle of the
panel with a tack. Here it is, and you can read for yourself."
He held up a piece of white cardboard about the size of a sheet
of note-paper. It read in this fashion:
THE RED-HEADED LEAGUE
IS
DISSOLVED.
October 9, 1890.
Sherlock Holmes and I surveyed this curt announcement and the
rueful face behind it, until the comical side of the affair so
completely overtopped every other consideration that we both
burst out into a roar of laughter.
"I cannot see that there is anything very funny," cried our
client, flushing up to the roots of his flaming head. "If you can
do nothing better than laugh at me, I can go elsewhere."
"No, no," cried Holmes, shoving him back into the chair from
which he had half risen. "I really wouldn't miss your case for
the world. It is most refreshingly unusual. But there is, if you
will excuse my saying so, something just a little funny about it.
Pray what steps did you take when you found the card upon the
door?"
"I was staggered, sir. I did not know what to do. Then I called
at the offices round, but none of them seemed to know anything
about it. Finally, I went to the landlord, who is an accountant
living on the ground-floor, and I asked him if he could tell me
what had become of the Red-headed League. He said that he had
never heard of any such body. Then I asked him who Mr. Duncan
Ross was. He answered that the name was new to him.
"'Well,' said I, 'the gentleman at No. 4.'
"'What, the red-headed man?'
"'Yes.'
"'Oh,' said he, 'his name was William Morris. He was a solicitor
and was using my room as a temporary convenience until his new
premises were ready. He moved out yesterday.'
"'Where could I find him?'
"'Oh, at his new offices. He did tell me the address. Yes, 17
King Edward Street, near St. Paul's.'
"I started off, Mr. Holmes, but when I got to that address it was
a manufactory of artificial knee-caps, and no one in it had ever
heard of either Mr. William Morris or Mr. Duncan Ross."
"And what did you do then?" asked Holmes.
"I went home to Saxe-Coburg Square, and I took the advice of my
assistant. But he could not help me in any way. He could only say
that if I waited I should hear by post. But that was not quite
good enough, Mr. Holmes. I did not wish to lose such a place
without a struggle, so, as I had heard that you were good enough
to give advice to poor folk who were in need of it, I came right
away to you."
"And you did very wisely," said Holmes. "Your case is an
exceedingly remarkable one, and I shall be happy to look into it.
From what you have told me I think that it is possible that
graver issues hang from it than might at first sight appear."
"Grave enough!" said Mr. Jabez Wilson. "Why, I have lost four
pound a week."
"As far as you are personally concerned," remarked Holmes, "I do
not see that you have any grievance against this extraordinary
league. On the contrary, you are, as I understand, richer by some
30 pounds, to say nothing of the minute knowledge which you have
gained on every subject which comes under the letter A. You have
lost nothing by them."
"No, sir. But I want to find out about them, and who they are,
and what their object was in playing this prank--if it was a
prank--upon me. It was a pretty expensive joke for them, for it
cost them two and thirty pounds."
"We shall endeavour to clear up these points for you. And, first,
one or two questions, Mr. Wilson. This assistant of yours who
first called your attention to the advertisement--how long had he
been with you?"
"About a month then."
"How did he come?"
"In answer to an advertisement."
"Was he the only applicant?"
"No, I had a dozen."
"Why did you pick him?"
"Because he was handy and would come cheap."
"At half-wages, in fact."
"Yes."
"What is he like, this Vincent Spaulding?"
"Small, stout-built, very quick in his ways, no hair on his face,
though he's not short of thirty. Has a white splash of acid upon
his forehead."
Holmes sat up in his chair in considerable excitement. "I thought
as much," said he. "Have you ever observed that his ears are
pierced for earrings?"
"Yes, sir. He told me that a gipsy had done it for him when he
was a lad."
"Hum!" said Holmes, sinking back in deep thought. "He is still
with you?"
"Oh, yes, sir; I have only just left him."
"And has your business been attended to in your absence?"
"Nothing to complain of, sir. There's never very much to do of a
morning."
"That will do, Mr. Wilson. I shall be happy to give you an
opinion upon the subject in the course of a day or two. To-day is
Saturday, and I hope that by Monday we may come to a conclusion."
"Well, Watson," said Holmes when our visitor had left us, "what
do you make of it all?"
"I make nothing of it," I answered frankly. "It is a most
mysterious business."
"As a rule," said Holmes, "the more bizarre a thing is the less
mysterious it proves to be. It is your commonplace, featureless
crimes which are really puzzling, just as a commonplace face is
the most difficult to identify. But I must be prompt over this
matter."
"What are you going to do, then?" I asked.
"To smoke," he answered. "It is quite a three pipe problem, and I
beg that you won't speak to me for fifty minutes." He curled
himself up in his chair, with his thin knees drawn up to his
hawk-like nose, and there he sat with his eyes closed and his
black clay pipe thrusting out like the bill of some strange bird.
I had come to the conclusion that he had dropped asleep, and
indeed was nodding myself, when he suddenly sprang out of his
chair with the gesture of a man who has made up his mind and put
his pipe down upon the mantelpiece.
"Sarasate plays at the St. James's Hall this afternoon," he
remarked. "What do you think, Watson? Could your patients spare
you for a few hours?"
"I have nothing to do to-day. My practice is never very
absorbing."
"Then put on your hat and come. I am going through the City
first, and we can have some lunch on the way. I observe that
there is a good deal of German music on the programme, which is
rather more to my taste than Italian or French. It is
introspective, and I want to introspect. Come along!"
We travelled by the Underground as far as Aldersgate; and a short
walk took us to Saxe-Coburg Square, the scene of the singular
story which we had listened to in the morning. It was a poky,
little, shabby-genteel place, where four lines of dingy
two-storied brick houses looked out into a small railed-in
enclosure, where a lawn of weedy grass and a few clumps of faded
laurel-bushes made a hard fight against a smoke-laden and
uncongenial atmosphere. Three gilt balls and a brown board with
"JABEZ WILSON" in white letters, upon a corner house, announced
the place where our red-headed client carried on his business.
Sherlock Holmes stopped in front of it with his head on one side
and looked it all over, with his eyes shining brightly between
puckered lids. Then he walked slowly up the street, and then down
again to the corner, still looking keenly at the houses. Finally
he returned to the pawnbroker's, and, having thumped vigorously
upon the pavement with his stick two or three times, he went up
to the door and knocked. It was instantly opened by a
bright-looking, clean-shaven young fellow, who asked him to step
in.
"Thank you," said Holmes, "I only wished to ask you how you would
go from here to the Strand."
"Third right, fourth left," answered the assistant promptly,
closing the door.
"Smart fellow, that," observed Holmes as we walked away. "He is,
in my judgment, the fourth smartest man in London, and for daring
I am not sure that he has not a claim to be third. I have known
something of him before."
"Evidently," said I, "Mr. Wilson's assistant counts for a good
deal in this mystery of the Red-headed League. I am sure that you
inquired your way merely in order that you might see him."
"Not him."
"What then?"
"The knees of his trousers."
"And what did you see?"
"What I expected to see."
"Why did you beat the pavement?"
"My dear doctor, this is a time for observation, not for talk. We
are spies in an enemy's country. We know something of Saxe-Coburg
Square. Let us now explore the parts which lie behind it."
The road in which we found ourselves as we turned round the
corner from the retired Saxe-Coburg Square presented as great a
contrast to it as the front of a picture does to the back. It was
one of the main arteries which conveyed the traffic of the City
to the north and west. The roadway was blocked with the immense
stream of commerce flowing in a double tide inward and outward,
while the footpaths were black with the hurrying swarm of
pedestrians. It was difficult to realise as we looked at the line
of fine shops and stately business premises that they really
abutted on the other side upon the faded and stagnant square
which we had just quitted.
"Let me see," said Holmes, standing at the corner and glancing
along the line, "I should like just to remember the order of the
houses here. It is a hobby of mine to have an exact knowledge of
London. There is Mortimer's, the tobacconist, the little
newspaper shop, the Coburg branch of the City and Suburban Bank,
the Vegetarian Restaurant, and McFarlane's carriage-building
depot. That carries us right on to the other block. And now,
Doctor, we've done our work, so it's time we had some play. A
sandwich and a cup of coffee, and then off to violin-land, where
all is sweetness and delicacy and harmony, and there are no
red-headed clients to vex us with their conundrums."
My friend was an enthusiastic musician, being himself not only a
very capable performer but a composer of no ordinary merit. All
the afternoon he sat in the stalls wrapped in the most perfect
happiness, gently waving his long, thin fingers in time to the
music, while his gently smiling face and his languid, dreamy eyes
were as unlike those of Holmes the sleuth-hound, Holmes the
relentless, keen-witted, ready-handed criminal agent, as it was
possible to conceive. In his singular character the dual nature
alternately asserted itself, and his extreme exactness and
astuteness represented, as I have often thought, the reaction
against the poetic and contemplative mood which occasionally
predominated in him. The swing of his nature took him from
extreme languor to devouring energy; and, as I knew well, he was
never so truly formidable as when, for days on end, he had been
lounging in his armchair amid his improvisations and his
black-letter editions. Then it was that the lust of the chase
would suddenly come upon him, and that his brilliant reasoning
power would rise to the level of intuition, until those who were
unacquainted with his methods would look askance at him as on a
man whose knowledge was not that of other mortals. When I saw him
that afternoon so enwrapped in the music at St. James's Hall I
felt that an evil time might be coming upon those whom he had set
himself to hunt down.
"You want to go home, no doubt, Doctor," he remarked as we
emerged.
"Yes, it would be as well."
"And I have some business to do which will take some hours. This
business at Coburg Square is serious."
"Why serious?"
"A considerable crime is in contemplation. I have every reason to
believe that we shall be in time to stop it. But to-day being
Saturday rather complicates matters. I shall want your help
to-night."
"At what time?"
"Ten will be early enough."
"I shall be at Baker Street at ten."
"Very well. And, I say, Doctor, there may be some little danger,
so kindly put your army revolver in your pocket." He waved his
hand, turned on his heel, and disappeared in an instant among the
crowd.
I trust that I am not more dense than my neighbours, but I was
always oppressed with a sense of my own stupidity in my dealings
with Sherlock Holmes. Here I had heard what he had heard, I had
seen what he had seen, and yet from his words it was evident that
he saw clearly not only what had happened but what was about to
happen, while to me the whole business was still confused and
grotesque. As I drove home to my house in Kensington I thought
over it all, from the extraordinary story of the red-headed
copier of the "Encyclopaedia" down to the visit to Saxe-Coburg
Square, and the ominous words with which he had parted from me.
What was this nocturnal expedition, and why should I go armed?
Where were we going, and what were we to do? I had the hint from
Holmes that this smooth-faced pawnbroker's assistant was a
formidable man--a man who might play a deep game. I tried to
puzzle it out, but gave it up in despair and set the matter aside
until night should bring an explanation.
It was a quarter-past nine when I started from home and made my
way across the Park, and so through Oxford Street to Baker
Street. Two hansoms were standing at the door, and as I entered
the passage I heard the sound of voices from above. On entering
his room I found Holmes in animated conversation with two men,
one of whom I recognised as Peter Jones, the official police
agent, while the other was a long, thin, sad-faced man, with a
very shiny hat and oppressively respectable frock-coat.
"Ha! Our party is complete," said Holmes, buttoning up his
pea-jacket and taking his heavy hunting crop from the rack.
"Watson, I think you know Mr. Jones, of Scotland Yard? Let me
introduce you to Mr. Merryweather, who is to be our companion in
to-night's adventure."
"We're hunting in couples again, Doctor, you see," said Jones in
his consequential way. "Our friend here is a wonderful man for
starting a chase. All he wants is an old dog to help him to do
the running down."
"I hope a wild goose may not prove to be the end of our chase,"
observed Mr. Merryweather gloomily.
"You may place considerable confidence in Mr. Holmes, sir," said
the police agent loftily. "He has his own little methods, which
are, if he won't mind my saying so, just a little too theoretical
and fantastic, but he has the makings of a detective in him. It
is not too much to say that once or twice, as in that business of
the Sholto murder and the Agra treasure, he has been more nearly
correct than the official force."
"Oh, if you say so, Mr. Jones, it is all right," said the
stranger with deference. "Still, I confess that I miss my rubber.
It is the first Saturday night for seven-and-twenty years that I
have not had my rubber."
"I think you will find," said Sherlock Holmes, "that you will
play for a higher stake to-night than you have ever done yet, and
that the play will be more exciting. For you, Mr. Merryweather,
the stake will be some 30,000 pounds; and for you, Jones, it will
be the man upon whom you wish to lay your hands."
"John Clay, the murderer, thief, smasher, and forger. He's a
young man, Mr. Merryweather, but he is at the head of his
profession, and I would rather have my bracelets on him than on
any criminal in London. He's a remarkable man, is young John
Clay. His grandfather was a royal duke, and he himself has been
to Eton and Oxford. His brain is as cunning as his fingers, and
though we meet signs of him at every turn, we never know where to
find the man himself. He'll crack a crib in Scotland one week,
and be raising money to build an orphanage in Cornwall the next.
I've been on his track for years and have never set eyes on him
yet."
"I hope that I may have the pleasure of introducing you to-night.
I've had one or two little turns also with Mr. John Clay, and I
agree with you that he is at the head of his profession. It is
past ten, however, and quite time that we started. If you two
will take the first hansom, Watson and I will follow in the
second."
Sherlock Holmes was not very communicative during the long drive
and lay back in the cab humming the tunes which he had heard in
the afternoon. We rattled through an endless labyrinth of gas-lit
streets until we emerged into Farrington Street.
"We are close there now," my friend remarked. "This fellow
Merryweather is a bank director, and personally interested in the
matter. I thought it as well to have Jones with us also. He is
not a bad fellow, though an absolute imbecile in his profession.
He has one positive virtue. He is as brave as a bulldog and as
tenacious as a lobster if he gets his claws upon anyone. Here we
are, and they are waiting for us."
We had reached the same crowded thoroughfare in which we had
found ourselves in the morning. Our cabs were dismissed, and,
following the guidance of Mr. Merryweather, we passed down a
narrow passage and through a side door, which he opened for us.
Within there was a small corridor, which ended in a very massive
iron gate. This also was opened, and led down a flight of winding
stone steps, which terminated at another formidable gate. Mr.
Merryweather stopped to light a lantern, and then conducted us
down a dark, earth-smelling passage, and so, after opening a
third door, into a huge vault or cellar, which was piled all
round with crates and massive boxes.
"You are not very vulnerable from above," Holmes remarked as he
held up the lantern and gazed about him.
"Nor from below," said Mr. Merryweather, striking his stick upon
the flags which lined the floor. "Why, dear me, it sounds quite
hollow!" he remarked, looking up in surprise.
"I must really ask you to be a little more quiet!" said Holmes
severely. "You have already imperilled the whole success of our
expedition. Might I beg that you would have the goodness to sit
down upon one of those boxes, and not to interfere?"
The solemn Mr. Merryweather perched himself upon a crate, with a
very injured expression upon his face, while Holmes fell upon his
knees upon the floor and, with the lantern and a magnifying lens,
began to examine minutely the cracks between the stones. A few
seconds sufficed to satisfy him, for he sprang to his feet again
and put his glass in his pocket.
"We have at least an hour before us," he remarked, "for they can
hardly take any steps until the good pawnbroker is safely in bed.
Then they will not lose a minute, for the sooner they do their
work the longer time they will have for their escape. We are at
present, Doctor--as no doubt you have divined--in the cellar of
the City branch of one of the principal London banks. Mr.
Merryweather is the chairman of directors, and he will explain to
you that there are reasons why the more daring criminals of
London should take a considerable interest in this cellar at
present."
"It is our French gold," whispered the director. "We have had
several warnings that an attempt might be made upon it."
"Your French gold?"
"Yes. We had occasion some months ago to strengthen our resources
and borrowed for that purpose 30,000 napoleons from the Bank of
France. It has become known that we have never had occasion to
unpack the money, and that it is still lying in our cellar. The
crate upon which I sit contains 2,000 napoleons packed between
layers of lead foil. Our reserve of bullion is much larger at
present than is usually kept in a single branch office, and the
directors have had misgivings upon the subject."
"Which were very well justified," observed Holmes. "And now it is
time that we arranged our little plans. I expect that within an
hour matters will come to a head. In the meantime Mr.
Merryweather, we must put the screen over that dark lantern."
"And sit in the dark?"
"I am afraid so. I had brought a pack of cards in my pocket, and
I thought that, as we were a partie carrée, you might have your
rubber after all. But I see that the enemy's preparations have
gone so far that we cannot risk the presence of a light. And,
first of all, we must choose our positions. These are daring men,
and though we shall take them at a disadvantage, they may do us
some harm unless we are careful. I shall stand behind this crate,
and do you conceal yourselves behind those. Then, when I flash a
light upon them, close in swiftly. If they fire, Watson, have no
compunction about shooting them down."
I placed my revolver, cocked, upon the top of the wooden case
behind which I crouched. Holmes shot the slide across the front
of his lantern and left us in pitch darkness--such an absolute
darkness as I have never before experienced. The smell of hot
metal remained to assure us that the light was still there, ready
to flash out at a moment's notice. To me, with my nerves worked
up to a pitch of expectancy, there was something depressing and
subduing in the sudden gloom, and in the cold dank air of the
vault.
"They have but one retreat," whispered Holmes. "That is back
through the house into Saxe-Coburg Square. I hope that you have
done what I asked you, Jones?"
"I have an inspector and two officers waiting at the front door."
"Then we have stopped all the holes. And now we must be silent
and wait."
What a time it seemed! From comparing notes afterwards it was but
an hour and a quarter, yet it appeared to me that the night must
have almost gone and the dawn be breaking above us. My limbs
were weary and stiff, for I feared to change my position; yet my
nerves were worked up to the highest pitch of tension, and my
hearing was so acute that I could not only hear the gentle
breathing of my companions, but I could distinguish the deeper,
heavier in-breath of the bulky Jones from the thin, sighing note
of the bank director. From my position I could look over the case
in the direction of the floor. Suddenly my eyes caught the glint
of a light.
At first it was but a lurid spark upon the stone pavement. Then
it lengthened out until it became a yellow line, and then,
without any warning or sound, a gash seemed to open and a hand
appeared, a white, almost womanly hand, which felt about in the
centre of the little area of light. For a minute or more the
hand, with its writhing fingers, protruded out of the floor. Then
it was withdrawn as suddenly as it appeared, and all was dark
again save the single lurid spark which marked a chink between
the stones.
Its disappearance, however, was but momentary. With a rending,
tearing sound, one of the broad, white stones turned over upon
its side and left a square, gaping hole, through which streamed
the light of a lantern. Over the edge there peeped a clean-cut,
boyish face, which looked keenly about it, and then, with a hand
on either side of the aperture, drew itself shoulder-high and
waist-high, until one knee rested upon the edge. In another
instant he stood at the side of the hole and was hauling after
him a companion, lithe and small like himself, with a pale face
and a shock of very red hair.
"It's all clear," he whispered. "Have you the chisel and the
bags? Great Scott! Jump, Archie, jump, and I'll swing for it!"
Sherlock Holmes had sprung out and seized the intruder by the
collar. The other dived down the hole, and I heard the sound of
rending cloth as Jones clutched at his skirts. The light flashed
upon the barrel of a revolver, but Holmes' hunting crop came
down on the man's wrist, and the pistol clinked upon the stone
floor.
"It's no use, John Clay," said Holmes blandly. "You have no
chance at all."
"So I see," the other answered with the utmost coolness. "I fancy
that my pal is all right, though I see you have got his
coat-tails."
"There are three men waiting for him at the door," said Holmes.
"Oh, indeed! You seem to have done the thing very completely. I
must compliment you."
"And I you," Holmes answered. "Your red-headed idea was very new
and effective."
"You'll see your pal again presently," said Jones. "He's quicker
at climbing down holes than I am. Just hold out while I fix the
derbies."
"I beg that you will not touch me with your filthy hands,"
remarked our prisoner as the handcuffs clattered upon his wrists.
"You may not be aware that I have royal blood in my veins. Have
the goodness, also, when you address me always to say 'sir' and
'please.'"
"All right," said Jones with a stare and a snigger. "Well, would
you please, sir, march upstairs, where we can get a cab to carry
your Highness to the police-station?"
"That is better," said John Clay serenely. He made a sweeping bow
to the three of us and walked quietly off in the custody of the
detective.
"Really, Mr. Holmes," said Mr. Merryweather as we followed them
from the cellar, "I do not know how the bank can thank you or
repay you. There is no doubt that you have detected and defeated
in the most complete manner one of the most determined attempts
at bank robbery that have ever come within my experience."
"I have had one or two little scores of my own to settle with Mr.
John Clay," said Holmes. "I have been at some small expense over
this matter, which I shall expect the bank to refund, but beyond
that I am amply repaid by having had an experience which is in
many ways unique, and by hearing the very remarkable narrative of
the Red-headed League."
"You see, Watson," he explained in the early hours of the morning
as we sat over a glass of whisky and soda in Baker Street, "it
was perfectly obvious from the first that the only possible
object of this rather fantastic business of the advertisement of
the League, and the copying of the 'Encyclopaedia,' must be to get
this not over-bright pawnbroker out of the way for a number of
hours every day. It was a curious way of managing it, but,
really, it would be difficult to suggest a better. The method was
no doubt suggested to Clay's ingenious mind by the colour of his
accomplice's hair. The 4 pounds a week was a lure which must draw
him, and what was it to them, who were playing for thousands?
They put in the advertisement, one rogue has the temporary
office, the other rogue incites the man to apply for it, and
together they manage to secure his absence every morning in the
week. From the time that I heard of the assistant having come for
half wages, it was obvious to me that he had some strong motive
for securing the situation."
"But how could you guess what the motive was?"
"Had there been women in the house, I should have suspected a
mere vulgar intrigue. That, however, was out of the question. The
man's business was a small one, and there was nothing in his
house which could account for such elaborate preparations, and
such an expenditure as they were at. It must, then, be something
out of the house. What could it be? I thought of the assistant's
fondness for photography, and his trick of vanishing into the
cellar. The cellar! There was the end of this tangled clue. Then
I made inquiries as to this mysterious assistant and found that I
had to deal with one of the coolest and most daring criminals in
London. He was doing something in the cellar--something which
took many hours a day for months on end. What could it be, once
more? I could think of nothing save that he was running a tunnel
to some other building.
"So far I had got when we went to visit the scene of action. I
surprised you by beating upon the pavement with my stick. I was
ascertaining whether the cellar stretched out in front or behind.
It was not in front. Then I rang the bell, and, as I hoped, the
assistant answered it. We have had some skirmishes, but we had
never set eyes upon each other before. I hardly looked at his
face. His knees were what I wished to see. You must yourself have
remarked how worn, wrinkled, and stained they were. They spoke of
those hours of burrowing. The only remaining point was what they
were burrowing for. I walked round the corner, saw the City and
Suburban Bank abutted on our friend's premises, and felt that I
had solved my problem. When you drove home after the concert I
called upon Scotland Yard and upon the chairman of the bank
directors, with the result that you have seen."
"And how could you tell that they would make their attempt
to-night?" I asked.
"Well, when they closed their League offices that was a sign that
they cared no longer about Mr. Jabez Wilson's presence--in other
words, that they had completed their tunnel. But it was essential
that they should use it soon, as it might be discovered, or the
bullion might be removed. Saturday would suit them better than
any other day, as it would give them two days for their escape.
For all these reasons I expected them to come to-night."
"You reasoned it out beautifully," I exclaimed in unfeigned
admiration. "It is so long a chain, and yet every link rings
true."
"It saved me from ennui," he answered, yawning. "Alas! I already
feel it closing in upon me. My life is spent in one long effort
to escape from the commonplaces of existence. These little
problems help me to do so."
"And you are a benefactor of the race," said I.
He shrugged his shoulders. "Well, perhaps, after all, it is of
some little use," he remarked. "'L'homme c'est rien--l'oeuvre
c'est tout,' as Gustave Flaubert wrote to George Sand."
ADVENTURE III. A CASE OF IDENTITY
"My dear fellow," said Sherlock Holmes as we sat on either side
of the fire in his lodgings at Baker Street, "life is infinitely
stranger than anything which the mind of man could invent. We
would not dare to conceive the things which are really mere
commonplaces of existence. If we could fly out of that window
hand in hand, hover over this great city, gently remove the
roofs, and peep in at the queer things which are going on, the
strange coincidences, the plannings, the cross-purposes, the
wonderful chains of events, working through generations, and
leading to the most outré results, it would make all fiction with
its conventionalities and foreseen conclusions most stale and
unprofitable."
"And yet I am not convinced of it," I answered. "The cases which
come to light in the papers are, as a rule, bald enough, and
vulgar enough. We have in our police reports realism pushed to
its extreme limits, and yet the result is, it must be confessed,
neither fascinating nor artistic."
"A certain selection and discretion must be used in producing a
realistic effect," remarked Holmes. "This is wanting in the
police report, where more stress is laid, perhaps, upon the
platitudes of the magistrate than upon the details, which to an
observer contain the vital essence of the whole matter. Depend
upon it, there is nothing so unnatural as the commonplace."
I smiled and shook my head. "I can quite understand your thinking
so," I said. "Of course, in your position of unofficial adviser
and helper to everybody who is absolutely puzzled, throughout
three continents, you are brought in contact with all that is
strange and bizarre. But here"--I picked up the morning paper
from the ground--"let us put it to a practical test. Here is the
first heading upon which I come. 'A husband's cruelty to his
wife.' There is half a column of print, but I know without
reading it that it is all perfectly familiar to me. There is, of
course, the other woman, the drink, the push, the blow, the
bruise, the sympathetic sister or landlady. The crudest of
writers could invent nothing more crude."
"Indeed, your example is an unfortunate one for your argument,"
said Holmes, taking the paper and glancing his eye down it. "This
is the Dundas separation case, and, as it happens, I was engaged
in clearing up some small points in connection with it. The
husband was a teetotaler, there was no other woman, and the
conduct complained of was that he had drifted into the habit of
winding up every meal by taking out his false teeth and hurling
them at his wife, which, you will allow, is not an action likely
to occur to the imagination of the average story-teller. Take a
pinch of snuff, Doctor, and acknowledge that I have scored over
you in your example."
He held out his snuffbox of old gold, with a great amethyst in
the centre of the lid. Its splendour was in such contrast to his
homely ways and simple life that I could not help commenting upon
it.
"Ah," said he, "I forgot that I had not seen you for some weeks.
It is a little souvenir from the King of Bohemia in return for my
assistance in the case of the Irene Adler papers."
"And the ring?" I asked, glancing at a remarkable brilliant which
sparkled upon his finger.
"It was from the reigning family of Holland, though the matter in
which I served them was of such delicacy that I cannot confide it
even to you, who have been good enough to chronicle one or two of
my little problems."
"And have you any on hand just now?" I asked with interest.
"Some ten or twelve, but none which present any feature of
interest. They are important, you understand, without being
interesting. Indeed, I have found that it is usually in
unimportant matters that there is a field for the observation,
and for the quick analysis of cause and effect which gives the
charm to an investigation. The larger crimes are apt to be the
simpler, for the bigger the crime the more obvious, as a rule, is
the motive. In these cases, save for one rather intricate matter
which has been referred to me from Marseilles, there is nothing
which presents any features of interest. It is possible, however,
that I may have something better before very many minutes are
over, for this is one of my clients, or I am much mistaken."
He had risen from his chair and was standing between the parted
blinds gazing down into the dull neutral-tinted London street.
Looking over his shoulder, I saw that on the pavement opposite
there stood a large woman with a heavy fur boa round her neck,
and a large curling red feather in a broad-brimmed hat which was
tilted in a coquettish Duchess of Devonshire fashion over her
ear. From under this great panoply she peeped up in a nervous,
hesitating fashion at our windows, while her body oscillated
backward and forward, and her fingers fidgeted with her glove
buttons. Suddenly, with a plunge, as of the swimmer who leaves
the bank, she hurried across the road, and we heard the sharp
clang of the bell.
"I have seen those symptoms before," said Holmes, throwing his
cigarette into the fire. "Oscillation upon the pavement always
means an affaire de coeur. She would like advice, but is not sure
that the matter is not too delicate for communication. And yet
even here we may discriminate. When a woman has been seriously
wronged by a man she no longer oscillates, and the usual symptom
is a broken bell wire. Here we may take it that there is a love
matter, but that the maiden is not so much angry as perplexed, or
grieved. But here she comes in person to resolve our doubts."
As he spoke there was a tap at the door, and the boy in buttons
entered to announce Miss Mary Sutherland, while the lady herself
loomed behind his small black figure like a full-sailed
merchant-man behind a tiny pilot boat. Sherlock Holmes welcomed
her with the easy courtesy for which he was remarkable, and,
having closed the door and bowed her into an armchair, he looked
her over in the minute and yet abstracted fashion which was
peculiar to him.
"Do you not find," he said, "that with your short sight it is a
little trying to do so much typewriting?"
"I did at first," she answered, "but now I know where the letters
are without looking." Then, suddenly realising the full purport
of his words, she gave a violent start and looked up, with fear
and astonishment upon her broad, good-humoured face. "You've
heard about me, Mr. Holmes," she cried, "else how could you know
all that?"
"Never mind," said Holmes, laughing; "it is my business to know
things. Perhaps I have trained myself to see what others
overlook. If not, why should you come to consult me?"
"I came to you, sir, because I heard of you from Mrs. Etherege,
whose husband you found so easy when the police and everyone had
given him up for dead. Oh, Mr. Holmes, I wish you would do as
much for me. I'm not rich, but still I have a hundred a year in
my own right, besides the little that I make by the machine, and
I would give it all to know what has become of Mr. Hosmer Angel."
"Why did you come away to consult me in such a hurry?" asked
Sherlock Holmes, with his finger-tips together and his eyes to
the ceiling.
Again a startled look came over the somewhat vacuous face of Miss
Mary Sutherland. "Yes, I did bang out of the house," she said,
"for it made me angry to see the easy way in which Mr.
Windibank--that is, my father--took it all. He would not go to
the police, and he would not go to you, and so at last, as he
would do nothing and kept on saying that there was no harm done,
it made me mad, and I just on with my things and came right away
to you."
"Your father," said Holmes, "your stepfather, surely, since the
name is different."
"Yes, my stepfather. I call him father, though it sounds funny,
too, for he is only five years and two months older than myself."
"And your mother is alive?"
"Oh, yes, mother is alive and well. I wasn't best pleased, Mr.
Holmes, when she married again so soon after father's death, and
a man who was nearly fifteen years younger than herself. Father
was a plumber in the Tottenham Court Road, and he left a tidy
business behind him, which mother carried on with Mr. Hardy, the
foreman; but when Mr. Windibank came he made her sell the
business, for he was very superior, being a traveller in wines.
They got 4700 pounds for the goodwill and interest, which wasn't
near as much as father could have got if he had been alive."
I had expected to see Sherlock Holmes impatient under this
rambling and inconsequential narrative, but, on the contrary, he
had listened with the greatest concentration of attention.
"Your own little income," he asked, "does it come out of the
business?"
"Oh, no, sir. It is quite separate and was left me by my uncle
Ned in Auckland. It is in New Zealand stock, paying 4 1/2 per
cent. Two thousand five hundred pounds was the amount, but I can
only touch the interest."
"You interest me extremely," said Holmes. "And since you draw so
large a sum as a hundred a year, with what you earn into the
bargain, you no doubt travel a little and indulge yourself in
every way. I believe that a single lady can get on very nicely
upon an income of about 60 pounds."
"I could do with much less than that, Mr. Holmes, but you
understand that as long as I live at home I don't wish to be a
burden to them, and so they have the use of the money just while
I am staying with them. Of course, that is only just for the
time. Mr. Windibank draws my interest every quarter and pays it
over to mother, and I find that I can do pretty well with what I
earn at typewriting. It brings me twopence a sheet, and I can
often do from fifteen to twenty sheets in a day."
"You have made your position very clear to me," said Holmes.
"This is my friend, Dr. Watson, before whom you can speak as
freely as before myself. Kindly tell us now all about your
connection with Mr. Hosmer Angel."
A flush stole over Miss Sutherland's face, and she picked
nervously at the fringe of her jacket. "I met him first at the
gasfitters' ball," she said. "They used to send father tickets
when he was alive, and then afterwards they remembered us, and
sent them to mother. Mr. Windibank did not wish us to go. He
never did wish us to go anywhere. He would get quite mad if I
wanted so much as to join a Sunday-school treat. But this time I
was set on going, and I would go; for what right had he to
prevent? He said the folk were not fit for us to know, when all
father's friends were to be there. And he said that I had nothing
fit to wear, when I had my purple plush that I had never so much
as taken out of the drawer. At last, when nothing else would do,
he went off to France upon the business of the firm, but we went,
mother and I, with Mr. Hardy, who used to be our foreman, and it
was there I met Mr. Hosmer Angel."
"I suppose," said Holmes, "that when Mr. Windibank came back from
France he was very annoyed at your having gone to the ball."
"Oh, well, he was very good about it. He laughed, I remember, and
shrugged his shoulders, and said there was no use denying
anything to a woman, for she would have her way."
"I see. Then at the gasfitters' ball you met, as I understand, a
gentleman called Mr. Hosmer Angel."
"Yes, sir. I met him that night, and he called next day to ask if
we had got home all safe, and after that we met him--that is to
say, Mr. Holmes, I met him twice for walks, but after that father
came back again, and Mr. Hosmer Angel could not come to the house
any more."
"No?"
"Well, you know father didn't like anything of the sort. He
wouldn't have any visitors if he could help it, and he used to
say that a woman should be happy in her own family circle. But
then, as I used to say to mother, a woman wants her own circle to
begin with, and I had not got mine yet."
"But how about Mr. Hosmer Angel? Did he make no attempt to see
you?"
"Well, father was going off to France again in a week, and Hosmer
wrote and said that it would be safer and better not to see each
other until he had gone. We could write in the meantime, and he
used to write every day. I took the letters in in the morning, so
there was no need for father to know."
"Were you engaged to the gentleman at this time?"
"Oh, yes, Mr. Holmes. We were engaged after the first walk that
we took. Hosmer--Mr. Angel--was a cashier in an office in
Leadenhall Street--and--"
"What office?"
"That's the worst of it, Mr. Holmes, I don't know."
"Where did he live, then?"
"He slept on the premises."
"And you don't know his address?"
"No--except that it was Leadenhall Street."
"Where did you address your letters, then?"
"To the Leadenhall Street Post Office, to be left till called
for. He said that if they were sent to the office he would be
chaffed by all the other clerks about having letters from a lady,
so I offered to typewrite them, like he did his, but he wouldn't
have that, for he said that when I wrote them they seemed to come
from me, but when they were typewritten he always felt that the
machine had come between us. That will just show you how fond he
was of me, Mr. Holmes, and the little things that he would think
of."
"It was most suggestive," said Holmes. "It has long been an axiom
of mine that the little things are infinitely the most important.
Can you remember any other little things about Mr. Hosmer Angel?"
"He was a very shy man, Mr. Holmes. He would rather walk with me
in the evening than in the daylight, for he said that he hated to
be conspicuous. Very retiring and gentlemanly he was. Even his
voice was gentle. He'd had the quinsy and swollen glands when he
was young, he told me, and it had left him with a weak throat,
and a hesitating, whispering fashion of speech. He was always
well dressed, very neat and plain, but his eyes were weak, just
as mine are, and he wore tinted glasses against the glare."
"Well, and what happened when Mr. Windibank, your stepfather,
returned to France?"
"Mr. Hosmer Angel came to the house again and proposed that we
should marry before father came back. He was in dreadful earnest
and made me swear, with my hands on the Testament, that whatever
happened I would always be true to him. Mother said he was quite
right to make me swear, and that it was a sign of his passion.
Mother was all in his favour from the first and was even fonder
of him than I was. Then, when they talked of marrying within the
week, I began to ask about father; but they both said never to
mind about father, but just to tell him afterwards, and mother
said she would make it all right with him. I didn't quite like
that, Mr. Holmes. It seemed funny that I should ask his leave, as
he was only a few years older than me; but I didn't want to do
anything on the sly, so I wrote to father at Bordeaux, where the
company has its French offices, but the letter came back to me on
the very morning of the wedding."
"It missed him, then?"
"Yes, sir; for he had started to England just before it arrived."
"Ha! that was unfortunate. Your wedding was arranged, then, for
the Friday. Was it to be in church?"
"Yes, sir, but very quietly. It was to be at St. Saviour's, near
King's Cross, and we were to have breakfast afterwards at the St.
Pancras Hotel. Hosmer came for us in a hansom, but as there were
two of us he put us both into it and stepped himself into a
four-wheeler, which happened to be the only other cab in the
street. We got to the church first, and when the four-wheeler
drove up we waited for him to step out, but he never did, and
when the cabman got down from the box and looked there was no one
there! The cabman said that he could not imagine what had become
of him, for he had seen him get in with his own eyes. That was
last Friday, Mr. Holmes, and I have never seen or heard anything
since then to throw any light upon what became of him."
"It seems to me that you have been very shamefully treated," said
Holmes.
"Oh, no, sir! He was too good and kind to leave me so. Why, all
the morning he was saying to me that, whatever happened, I was to
be true; and that even if something quite unforeseen occurred to
separate us, I was always to remember that I was pledged to him,
and that he would claim his pledge sooner or later. It seemed
strange talk for a wedding-morning, but what has happened since
gives a meaning to it."
"Most certainly it does. Your own opinion is, then, that some
unforeseen catastrophe has occurred to him?"
"Yes, sir. I believe that he foresaw some danger, or else he
would not have talked so. And then I think that what he foresaw
happened."
"But you have no notion as to what it could have been?"
"None."
"One more question. How did your mother take the matter?"
"She was angry, and said that I was never to speak of the matter
again."
"And your father? Did you tell him?"
"Yes; and he seemed to think, with me, that something had
happened, and that I should hear of Hosmer again. As he said,
what interest could anyone have in bringing me to the doors of
the church, and then leaving me? Now, if he had borrowed my
money, or if he had married me and got my money settled on him,
there might be some reason, but Hosmer was very independent about
money and never would look at a shilling of mine. And yet, what
could have happened? And why could he not write? Oh, it drives me
half-mad to think of it, and I can't sleep a wink at night." She
pulled a little handkerchief out of her muff and began to sob
heavily into it.
"I shall glance into the case for you," said Holmes, rising, "and
I have no doubt that we shall reach some definite result. Let the
weight of the matter rest upon me now, and do not let your mind
dwell upon it further. Above all, try to let Mr. Hosmer Angel
vanish from your memory, as he has done from your life."
"Then you don't think I'll see him again?"
"I fear not."
"Then what has happened to him?"
"You will leave that question in my hands. I should like an
accurate description of him and any letters of his which you can
spare."
"I advertised for him in last Saturday's Chronicle," said she.
"Here is the slip and here are four letters from him."
"Thank you. And your address?"
"No. 31 Lyon Place, Camberwell."
"Mr. Angel's address you never had, I understand. Where is your
father's place of business?"
"He travels for Westhouse & Marbank, the great claret importers
of Fenchurch Street."
"Thank you. You have made your statement very clearly. You will
leave the papers here, and remember the advice which I have given
you. Let the whole incident be a sealed book, and do not allow it
to affect your life."
"You are very kind, Mr. Holmes, but I cannot do that. I shall be
true to Hosmer. He shall find me ready when he comes back."
For all the preposterous hat and the vacuous face, there was
something noble in the simple faith of our visitor which
compelled our respect. She laid her little bundle of papers upon
the table and went her way, with a promise to come again whenever
she might be summoned.
Sherlock Holmes sat silent for a few minutes with his fingertips
still pressed together, his legs stretched out in front of him,
and his gaze directed upward to the ceiling. Then he took down
from the rack the old and oily clay pipe, which was to him as a
counsellor, and, having lit it, he leaned back in his chair, with
the thick blue cloud-wreaths spinning up from him, and a look of
infinite languor in his face.
"Quite an interesting study, that maiden," he observed. "I found
her more interesting than her little problem, which, by the way,
is rather a trite one. You will find parallel cases, if you
consult my index, in Andover in '77, and there was something of
the sort at The Hague last year. Old as is the idea, however,
there were one or two details which were new to me. But the
maiden herself was most instructive."
"You appeared to read a good deal upon her which was quite
invisible to me," I remarked.
"Not invisible but unnoticed, Watson. You did not know where to
look, and so you missed all that was important. I can never bring
you to realise the importance of sleeves, the suggestiveness of
thumb-nails, or the great issues that may hang from a boot-lace.
Now, what did you gather from that woman's appearance? Describe
it."
"Well, she had a slate-coloured, broad-brimmed straw hat, with a
feather of a brickish red. Her jacket was black, with black beads
sewn upon it, and a fringe of little black jet ornaments. Her
dress was brown, rather darker than coffee colour, with a little
purple plush at the neck and sleeves. Her gloves were greyish and
were worn through at the right forefinger. Her boots I didn't
observe. She had small round, hanging gold earrings, and a
general air of being fairly well-to-do in a vulgar, comfortable,
easy-going way."
Sherlock Holmes clapped his hands softly together and chuckled.
"'Pon my word, Watson, you are coming along wonderfully. You have
really done very well indeed. It is true that you have missed
everything of importance, but you have hit upon the method, and
you have a quick eye for colour. Never trust to general
impressions, my boy, but concentrate yourself upon details. My
first glance is always at a woman's sleeve. In a man it is
perhaps better first to take the knee of the trouser. As you
observe, this woman had plush upon her sleeves, which is a most
useful material for showing traces. The double line a little
above the wrist, where the typewritist presses against the table,
was beautifully defined. The sewing-machine, of the hand type,
leaves a similar mark, but only on the left arm, and on the side
of it farthest from the thumb, instead of being right across the
broadest part, as this was. I then glanced at her face, and,
observing the dint of a pince-nez at either side of her nose, I
ventured a remark upon short sight and typewriting, which seemed
to surprise her."
"It surprised me."
"But, surely, it was obvious. I was then much surprised and
interested on glancing down to observe that, though the boots
which she was wearing were not unlike each other, they were
really odd ones; the one having a slightly decorated toe-cap, and
the other a plain one. One was buttoned only in the two lower
buttons out of five, and the other at the first, third, and
fifth. Now, when you see that a young lady, otherwise neatly
dressed, has come away from home with odd boots, half-buttoned,
it is no great deduction to say that she came away in a hurry."
"And what else?" I asked, keenly interested, as I always was, by
my friend's incisive reasoning.
"I noted, in passing, that she had written a note before leaving
home but after being fully dressed. You observed that her right
glove was torn at the forefinger, but you did not apparently see
that both glove and finger were stained with violet ink. She had
written in a hurry and dipped her pen too deep. It must have been
this morning, or the mark would not remain clear upon the finger.
All this is amusing, though rather elementary, but I must go back
to business, Watson. Would you mind reading me the advertised
description of Mr. Hosmer Angel?"
I held the little printed slip to the light.
"Missing," it said, "on the morning of the fourteenth, a gentleman
named Hosmer Angel. About five ft. seven in. in height;
strongly built, sallow complexion, black hair, a little bald in
the centre, bushy, black side-whiskers and moustache; tinted
glasses, slight infirmity of speech. Was dressed, when last seen,
in black frock-coat faced with silk, black waistcoat, gold Albert
chain, and grey Harris tweed trousers, with brown gaiters over
elastic-sided boots. Known to have been employed in an office in
Leadenhall Street. Anybody bringing--"
"That will do," said Holmes. "As to the letters," he continued,
glancing over them, "they are very commonplace. Absolutely no
clue in them to Mr. Angel, save that he quotes Balzac once. There
is one remarkable point, however, which will no doubt strike
you."
"They are typewritten," I remarked.
"Not only that, but the signature is typewritten. Look at the
neat little 'Hosmer Angel' at the bottom. There is a date, you
see, but no superscription except Leadenhall Street, which is
rather vague. The point about the signature is very suggestive--in
fact, we may call it conclusive."
"Of what?"
"My dear fellow, is it possible you do not see how strongly it
bears upon the case?"
"I cannot say that I do unless it were that he wished to be able
to deny his signature if an action for breach of promise were
instituted."
"No, that was not the point. However, I shall write two letters,
which should settle the matter. One is to a firm in the City, the
other is to the young lady's stepfather, Mr. Windibank, asking
him whether he could meet us here at six o'clock tomorrow
evening. It is just as well that we should do business with the
male relatives. And now, Doctor, we can do nothing until the
answers to those letters come, so we may put our little problem
upon the shelf for the interim."
I had had so many reasons to believe in my friend's subtle powers
of reasoning and extraordinary energy in action that I felt that
he must have some solid grounds for the assured and easy
demeanour with which he treated the singular mystery which he had
been called upon to fathom. Once only had I known him to fail, in
the case of the King of Bohemia and of the Irene Adler
photograph; but when I looked back to the weird business of the
Sign of Four, and the extraordinary circumstances connected with
the Study in Scarlet, I felt that it would be a strange tangle
indeed which he could not unravel.
I left him then, still puffing at his black clay pipe, with the
conviction that when I came again on the next evening I would
find that he held in his hands all the clues which would lead up
to the identity of the disappearing bridegroom of Miss Mary
Sutherland.
A professional case of great gravity was engaging my own
attention at the time, and the whole of next day I was busy at
the bedside of the sufferer. It was not until close upon six
o'clock that I found myself free and was able to spring into a
hansom and drive to Baker Street, half afraid that I might be too
late to assist at the dénouement of the little mystery. I found
Sherlock Holmes alone, however, half asleep, with his long, thin
form curled up in the recesses of his armchair. A formidable
array of bottles and test-tubes, with the pungent cleanly smell
of hydrochloric acid, told me that he had spent his day in the
chemical work which was so dear to him.
"Well, have you solved it?" I asked as I entered.
"Yes. It was the bisulphate of baryta."
"No, no, the mystery!" I cried.
"Oh, that! I thought of the salt that I have been working upon.
There was never any mystery in the matter, though, as I said
yesterday, some of the details are of interest. The only drawback
is that there is no law, I fear, that can touch the scoundrel."
"Who was he, then, and what was his object in deserting Miss
Sutherland?"
The question was hardly out of my mouth, and Holmes had not yet
opened his lips to reply, when we heard a heavy footfall in the
passage and a tap at the door.
"This is the girl's stepfather, Mr. James Windibank," said
Holmes. "He has written to me to say that he would be here at
six. Come in!"
The man who entered was a sturdy, middle-sized fellow, some
thirty years of age, clean-shaven, and sallow-skinned, with a
bland, insinuating manner, and a pair of wonderfully sharp and
penetrating grey eyes. He shot a questioning glance at each of
us, placed his shiny top-hat upon the sideboard, and with a
slight bow sidled down into the nearest chair.
"Good-evening, Mr. James Windibank," said Holmes. "I think that
this typewritten letter is from you, in which you made an
appointment with me for six o'clock?"
"Yes, sir. I am afraid that I am a little late, but I am not
quite my own master, you know. I am sorry that Miss Sutherland
has troubled you about this little matter, for I think it is far
better not to wash linen of the sort in public. It was quite
against my wishes that she came, but she is a very excitable,
impulsive girl, as you may have noticed, and she is not easily
controlled when she has made up her mind on a point. Of course, I
did not mind you so much, as you are not connected with the
official police, but it is not pleasant to have a family
misfortune like this noised abroad. Besides, it is a useless
expense, for how could you possibly find this Hosmer Angel?"
"On the contrary," said Holmes quietly; "I have every reason to
believe that I will succeed in discovering Mr. Hosmer Angel."
Mr. Windibank gave a violent start and dropped his gloves. "I am
delighted to hear it," he said.
"It is a curious thing," remarked Holmes, "that a typewriter has
really quite as much individuality as a man's handwriting. Unless
they are quite new, no two of them write exactly alike. Some
letters get more worn than others, and some wear only on one
side. Now, you remark in this note of yours, Mr. Windibank, that
in every case there is some little slurring over of the 'e,' and
a slight defect in the tail of the 'r.' There are fourteen other
characteristics, but those are the more obvious."
"We do all our correspondence with this machine at the office,
and no doubt it is a little worn," our visitor answered, glancing
keenly at Holmes with his bright little eyes.
"And now I will show you what is really a very interesting study,
Mr. Windibank," Holmes continued. "I think of writing another
little monograph some of these days on the typewriter and its
relation to crime. It is a subject to which I have devoted some
little attention. I have here four letters which purport to come
from the missing man. They are all typewritten. In each case, not
only are the 'e's' slurred and the 'r's' tailless, but you will
observe, if you care to use my magnifying lens, that the fourteen
other characteristics to which I have alluded are there as well."
Mr. Windibank sprang out of his chair and picked up his hat. "I
cannot waste time over this sort of fantastic talk, Mr. Holmes,"
he said. "If you can catch the man, catch him, and let me know
when you have done it."
"Certainly," said Holmes, stepping over and turning the key in
the door. "I let you know, then, that I have caught him!"
"What! where?" shouted Mr. Windibank, turning white to his lips
and glancing about him like a rat in a trap.
"Oh, it won't do--really it won't," said Holmes suavely. "There
is no possible getting out of it, Mr. Windibank. It is quite too
transparent, and it was a very bad compliment when you said that
it was impossible for me to solve so simple a question. That's
right! Sit down and let us talk it over."
Our visitor collapsed into a chair, with a ghastly face and a
glitter of moisture on his brow. "It--it's not actionable," he
stammered.
"I am very much afraid that it is not. But between ourselves,
Windibank, it was as cruel and selfish and heartless a trick in a
petty way as ever came before me. Now, let me just run over the
course of events, and you will contradict me if I go wrong."
The man sat huddled up in his chair, with his head sunk upon his
breast, like one who is utterly crushed. Holmes stuck his feet up
on the corner of the mantelpiece and, leaning back with his hands
in his pockets, began talking, rather to himself, as it seemed,
than to us.
"The man married a woman very much older than himself for her
money," said he, "and he enjoyed the use of the money of the
daughter as long as she lived with them. It was a considerable
sum, for people in their position, and the loss of it would have
made a serious difference. It was worth an effort to preserve it.
The daughter was of a good, amiable disposition, but affectionate
and warm-hearted in her ways, so that it was evident that with
her fair personal advantages, and her little income, she would
not be allowed to remain single long. Now her marriage would
mean, of course, the loss of a hundred a year, so what does her
stepfather do to prevent it? He takes the obvious course of
keeping her at home and forbidding her to seek the company of
people of her own age. But soon he found that that would not
answer forever. She became restive, insisted upon her rights, and
finally announced her positive intention of going to a certain
ball. What does her clever stepfather do then? He conceives an
idea more creditable to his head than to his heart. With the
connivance and assistance of his wife he disguised himself,
covered those keen eyes with tinted glasses, masked the face with
a moustache and a pair of bushy whiskers, sunk that clear voice
into an insinuating whisper, and doubly secure on account of the
girl's short sight, he appears as Mr. Hosmer Angel, and keeps off
other lovers by making love himself."
"It was only a joke at first," groaned our visitor. "We never
thought that she would have been so carried away."
"Very likely not. However that may be, the young lady was very
decidedly carried away, and, having quite made up her mind that
her stepfather was in France, the suspicion of treachery never
for an instant entered her mind. She was flattered by the
gentleman's attentions, and the effect was increased by the
loudly expressed admiration of her mother. Then Mr. Angel began
to call, for it was obvious that the matter should be pushed as
far as it would go if a real effect were to be produced. There
were meetings, and an engagement, which would finally secure the
girl's affections from turning towards anyone else. But the
deception could not be kept up forever. These pretended journeys
to France were rather cumbrous. The thing to do was clearly to
bring the business to an end in such a dramatic manner that it
would leave a permanent impression upon the young lady's mind and
prevent her from looking upon any other suitor for some time to
come. Hence those vows of fidelity exacted upon a Testament, and
hence also the allusions to a possibility of something happening
on the very morning of the wedding. James Windibank wished Miss
Sutherland to be so bound to Hosmer Angel, and so uncertain as to
his fate, that for ten years to come, at any rate, she would not
listen to another man. As far as the church door he brought her,
and then, as he could go no farther, he conveniently vanished
away by the old trick of stepping in at one door of a
four-wheeler and out at the other. I think that was the chain of
events, Mr. Windibank!"
Our visitor had recovered something of his assurance while Holmes
had been talking, and he rose from his chair now with a cold
sneer upon his pale face.
"It may be so, or it may not, Mr. Holmes," said he, "but if you
are so very sharp you ought to be sharp enough to know that it is
you who are breaking the law now, and not me. I have done nothing
actionable from the first, but as long as you keep that door
locked you lay yourself open to an action for assault and illegal
constraint."
"The law cannot, as you say, touch you," said Holmes, unlocking
and throwing open the door, "yet there never was a man who
deserved punishment more. If the young lady has a brother or a
friend, he ought to lay a whip across your shoulders. By Jove!"
he continued, flushing up at the sight of the bitter sneer upon
the man's face, "it is not part of my duties to my client, but
here's a hunting crop handy, and I think I shall just treat
myself to--" He took two swift steps to the whip, but before he
could grasp it there was a wild clatter of steps upon the stairs,
the heavy hall door banged, and from the window we could see Mr.
James Windibank running at the top of his speed down the road.
"There's a cold-blooded scoundrel!" said Holmes, laughing, as he
threw himself down into his chair once more. "That fellow will
rise from crime to crime until he does something very bad, and
ends on a gallows. The case has, in some respects, been not
entirely devoid of interest."
"I cannot now entirely see all the steps of your reasoning," I
remarked.
"Well, of course it was obvious from the first that this Mr.
Hosmer Angel must have some strong object for his curious
conduct, and it was equally clear that the only man who really
profited by the incident, as far as we could see, was the
stepfather. Then the fact that the two men were never together,
but that the one always appeared when the other was away, was
suggestive. So were the tinted spectacles and the curious voice,
which both hinted at a disguise, as did the bushy whiskers. My
suspicions were all confirmed by his peculiar action in
typewriting his signature, which, of course, inferred that his
handwriting was so familiar to her that she would recognise even
the smallest sample of it. You see all these isolated facts,
together with many minor ones, all pointed in the same
direction."
"And how did you verify them?"
"Having once spotted my man, it was easy to get corroboration. I
knew the firm for which this man worked. Having taken the printed
description. I eliminated everything from it which could be the
result of a disguise--the whiskers, the glasses, the voice, and I
sent it to the firm, with a request that they would inform me
whether it answered to the description of any of their
travellers. I had already noticed the peculiarities of the
typewriter, and I wrote to the man himself at his business
address asking him if he would come here. As I expected, his
reply was typewritten and revealed the same trivial but
characteristic defects. The same post brought me a letter from
Westhouse & Marbank, of Fenchurch Street, to say that the
description tallied in every respect with that of their employé,
James Windibank. Voilà tout!"
"And Miss Sutherland?"
"If I tell her she will not believe me. You may remember the old
Persian saying, 'There is danger for him who taketh the tiger
cub, and danger also for whoso snatches a delusion from a woman.'
There is as much sense in Hafiz as in Horace, and as much
knowledge of the world."
ADVENTURE IV. THE BOSCOMBE VALLEY MYSTERY
We were seated at breakfast one morning, my wife and I, when the
maid brought in a telegram. It was from Sherlock Holmes and ran
in this way:
"Have you a couple of days to spare? Have just been wired for from
the west of England in connection with Boscombe Valley tragedy.
Shall be glad if you will come with me. Air and scenery perfect.
Leave Paddington by the 11:15."
"What do you say, dear?" said my wife, looking across at me.
"Will you go?"
"I really don't know what to say. I have a fairly long list at
present."
"Oh, Anstruther would do your work for you. You have been looking
a little pale lately. I think that the change would do you good,
and you are always so interested in Mr. Sherlock Holmes' cases."
"I should be ungrateful if I were not, seeing what I gained
through one of them," I answered. "But if I am to go, I must pack
at once, for I have only half an hour."
My experience of camp life in Afghanistan had at least had the
effect of making me a prompt and ready traveller. My wants were
few and simple, so that in less than the time stated I was in a
cab with my valise, rattling away to Paddington Station. Sherlock
Holmes was pacing up and down the platform, his tall, gaunt
figure made even gaunter and taller by his long grey
travelling-cloak and close-fitting cloth cap.
"It is really very good of you to come, Watson," said he. "It
makes a considerable difference to me, having someone with me on
whom I can thoroughly rely. Local aid is always either worthless
or else biassed. If you will keep the two corner seats I shall
get the tickets."
We had the carriage to ourselves save for an immense litter of
papers which Holmes had brought with him. Among these he rummaged
and read, with intervals of note-taking and of meditation, until
we were past Reading. Then he suddenly rolled them all into a
gigantic ball and tossed them up onto the rack.
"Have you heard anything of the case?" he asked.
"Not a word. I have not seen a paper for some days."
"The London press has not had very full accounts. I have just
been looking through all the recent papers in order to master the
particulars. It seems, from what I gather, to be one of those
simple cases which are so extremely difficult."
"That sounds a little paradoxical."
"But it is profoundly true. Singularity is almost invariably a
clue. The more featureless and commonplace a crime is, the more
difficult it is to bring it home. In this case, however, they
have established a very serious case against the son of the
murdered man."
"It is a murder, then?"
"Well, it is conjectured to be so. I shall take nothing for
granted until I have the opportunity of looking personally into
it. I will explain the state of things to you, as far as I have
been able to understand it, in a very few words.
"Boscombe Valley is a country district not very far from Ross, in
Herefordshire. The largest landed proprietor in that part is a
Mr. John Turner, who made his money in Australia and returned
some years ago to the old country. One of the farms which he
held, that of Hatherley, was let to Mr. Charles McCarthy, who was
also an ex-Australian. The men had known each other in the
colonies, so that it was not unnatural that when they came to
settle down they should do so as near each other as possible.
Turner was apparently the richer man, so McCarthy became his
tenant but still remained, it seems, upon terms of perfect
equality, as they were frequently together. McCarthy had one son,
a lad of eighteen, and Turner had an only daughter of the same
age, but neither of them had wives living. They appear to have
avoided the society of the neighbouring English families and to
have led retired lives, though both the McCarthys were fond of
sport and were frequently seen at the race-meetings of the
neighbourhood. McCarthy kept two servants--a man and a girl.
Turner had a considerable household, some half-dozen at the
least. That is as much as I have been able to gather about the
families. Now for the facts.
"On June 3rd, that is, on Monday last, McCarthy left his house at
Hatherley about three in the afternoon and walked down to the
Boscombe Pool, which is a small lake formed by the spreading out
of the stream which runs down the Boscombe Valley. He had been
out with his serving-man in the morning at Ross, and he had told
the man that he must hurry, as he had an appointment of
importance to keep at three. From that appointment he never came
back alive.
"From Hatherley Farm-house to the Boscombe Pool is a quarter of a
mile, and two people saw him as he passed over this ground. One
was an old woman, whose name is not mentioned, and the other was
William Crowder, a game-keeper in the employ of Mr. Turner. Both
these witnesses depose that Mr. McCarthy was walking alone. The
game-keeper adds that within a few minutes of his seeing Mr.
McCarthy pass he had seen his son, Mr. James McCarthy, going the
same way with a gun under his arm. To the best of his belief, the
father was actually in sight at the time, and the son was
following him. He thought no more of the matter until he heard in
the evening of the tragedy that had occurred.
"The two McCarthys were seen after the time when William Crowder,
the game-keeper, lost sight of them. The Boscombe Pool is thickly
wooded round, with just a fringe of grass and of reeds round the
edge. A girl of fourteen, Patience Moran, who is the daughter of
the lodge-keeper of the Boscombe Valley estate, was in one of the
woods picking flowers. She states that while she was there she
saw, at the border of the wood and close by the lake, Mr.
McCarthy and his son, and that they appeared to be having a
violent quarrel. She heard Mr. McCarthy the elder using very
strong language to his son, and she saw the latter raise up his
hand as if to strike his father. She was so frightened by their
violence that she ran away and told her mother when she reached
home that she had left the two McCarthys quarrelling near
Boscombe Pool, and that she was afraid that they were going to
fight. She had hardly said the words when young Mr. McCarthy came
running up to the lodge to say that he had found his father dead
in the wood, and to ask for the help of the lodge-keeper. He was
much excited, without either his gun or his hat, and his right
hand and sleeve were observed to be stained with fresh blood. On
following him they found the dead body stretched out upon the
grass beside the pool. The head had been beaten in by repeated
blows of some heavy and blunt weapon. The injuries were such as
might very well have been inflicted by the butt-end of his son's
gun, which was found lying on the grass within a few paces of the
body. Under these circumstances the young man was instantly
arrested, and a verdict of 'wilful murder' having been returned
at the inquest on Tuesday, he was on Wednesday brought before the
magistrates at Ross, who have referred the case to the next
Assizes. Those are the main facts of the case as they came out
before the coroner and the police-court."
"I could hardly imagine a more damning case," I remarked. "If
ever circumstantial evidence pointed to a criminal it does so
here."
"Circumstantial evidence is a very tricky thing," answered Holmes
thoughtfully. "It may seem to point very straight to one thing,
but if you shift your own point of view a little, you may find it
pointing in an equally uncompromising manner to something
entirely different. It must be confessed, however, that the case
looks exceedingly grave against the young man, and it is very
possible that he is indeed the culprit. There are several people
in the neighbourhood, however, and among them Miss Turner, the
daughter of the neighbouring landowner, who believe in his
innocence, and who have retained Lestrade, whom you may recollect
in connection with the Study in Scarlet, to work out the case in
his interest. Lestrade, being rather puzzled, has referred the
case to me, and hence it is that two middle-aged gentlemen are
flying westward at fifty miles an hour instead of quietly
digesting their breakfasts at home."
"I am afraid," said I, "that the facts are so obvious that you
will find little credit to be gained out of this case."
"There is nothing more deceptive than an obvious fact," he
answered, laughing. "Besides, we may chance to hit upon some
other obvious facts which may have been by no means obvious to
Mr. Lestrade. You know me too well to think that I am boasting
when I say that I shall either confirm or destroy his theory by
means which he is quite incapable of employing, or even of
understanding. To take the first example to hand, I very clearly
perceive that in your bedroom the window is upon the right-hand
side, and yet I question whether Mr. Lestrade would have noted
even so self-evident a thing as that."
"How on earth--"
"My dear fellow, I know you well. I know the military neatness
which characterises you. You shave every morning, and in this
season you shave by the sunlight; but since your shaving is less
and less complete as we get farther back on the left side, until
it becomes positively slovenly as we get round the angle of the
jaw, it is surely very clear that that side is less illuminated
than the other. I could not imagine a man of your habits looking
at himself in an equal light and being satisfied with such a
result. I only quote this as a trivial example of observation and
inference. Therein lies my métier, and it is just possible that
it may be of some service in the investigation which lies before
us. There are one or two minor points which were brought out in
the inquest, and which are worth considering."
"What are they?"
"It appears that his arrest did not take place at once, but after
the return to Hatherley Farm. On the inspector of constabulary
informing him that he was a prisoner, he remarked that he was not
surprised to hear it, and that it was no more than his deserts.
This observation of his had the natural effect of removing any
traces of doubt which might have remained in the minds of the
coroner's jury."
"It was a confession," I ejaculated.
"No, for it was followed by a protestation of innocence."
"Coming on the top of such a damning series of events, it was at
least a most suspicious remark."
"On the contrary," said Holmes, "it is the brightest rift which I
can at present see in the clouds. However innocent he might be,
he could not be such an absolute imbecile as not to see that the
circumstances were very black against him. Had he appeared
surprised at his own arrest, or feigned indignation at it, I
should have looked upon it as highly suspicious, because such
surprise or anger would not be natural under the circumstances,
and yet might appear to be the best policy to a scheming man. His
frank acceptance of the situation marks him as either an innocent
man, or else as a man of considerable self-restraint and
firmness. As to his remark about his deserts, it was also not
unnatural if you consider that he stood beside the dead body of
his father, and that there is no doubt that he had that very day
so far forgotten his filial duty as to bandy words with him, and
even, according to the little girl whose evidence is so
important, to raise his hand as if to strike him. The
self-reproach and contrition which are displayed in his remark
appear to me to be the signs of a healthy mind rather than of a
guilty one."
I shook my head. "Many men have been hanged on far slighter
evidence," I remarked.
"So they have. And many men have been wrongfully hanged."
"What is the young man's own account of the matter?"
"It is, I am afraid, not very encouraging to his supporters,
though there are one or two points in it which are suggestive.
You will find it here, and may read it for yourself."
He picked out from his bundle a copy of the local Herefordshire
paper, and having turned down the sheet he pointed out the
paragraph in which the unfortunate young man had given his own
statement of what had occurred. I settled myself down in the
corner of the carriage and read it very carefully. It ran in this
way:
"Mr. James McCarthy, the only son of the deceased, was then called
and gave evidence as follows: 'I had been away from home for
three days at Bristol, and had only just returned upon the
morning of last Monday, the 3rd. My father was absent from home at
the time of my arrival, and I was informed by the maid that he
had driven over to Ross with John Cobb, the groom. Shortly after
my return I heard the wheels of his trap in the yard, and,
looking out of my window, I saw him get out and walk rapidly out
of the yard, though I was not aware in which direction he was
going. I then took my gun and strolled out in the direction of
the Boscombe Pool, with the intention of visiting the rabbit
warren which is upon the other side. On my way I saw William
Crowder, the game-keeper, as he had stated in his evidence; but
he is mistaken in thinking that I was following my father. I had
no idea that he was in front of me. When about a hundred yards
from the pool I heard a cry of "Cooee!" which was a usual signal
between my father and myself. I then hurried forward, and found
him standing by the pool. He appeared to be much surprised at
seeing me and asked me rather roughly what I was doing there. A
conversation ensued which led to high words and almost to blows,
for my father was a man of a very violent temper. Seeing that his
passion was becoming ungovernable, I left him and returned
towards Hatherley Farm. I had not gone more than 150 yards,
however, when I heard a hideous outcry behind me, which caused me
to run back again. I found my father expiring upon the ground,
with his head terribly injured. I dropped my gun and held him in
my arms, but he almost instantly expired. I knelt beside him for
some minutes, and then made my way to Mr. Turner's lodge-keeper,
his house being the nearest, to ask for assistance. I saw no one
near my father when I returned, and I have no idea how he came by
his injuries. He was not a popular man, being somewhat cold and
forbidding in his manners, but he had, as far as I know, no
active enemies. I know nothing further of the matter.'
"The Coroner: Did your father make any statement to you before
he died?
"Witness: He mumbled a few words, but I could only catch some
allusion to a rat.
"The Coroner: What did you understand by that?
"Witness: It conveyed no meaning to me. I thought that he was
delirious.
"The Coroner: What was the point upon which you and your father
had this final quarrel?
"Witness: I should prefer not to answer.
"The Coroner: I am afraid that I must press it.
"Witness: It is really impossible for me to tell you. I can
assure you that it has nothing to do with the sad tragedy which
followed.
"The Coroner: That is for the court to decide. I need not point
out to you that your refusal to answer will prejudice your case
considerably in any future proceedings which may arise.
"Witness: I must still refuse.
"The Coroner: I understand that the cry of 'Cooee' was a common
signal between you and your father?
"Witness: It was.
"The Coroner: How was it, then, that he uttered it before he saw
you, and before he even knew that you had returned from Bristol?
"Witness (with considerable confusion): I do not know.
"A Juryman: Did you see nothing which aroused your suspicions
when you returned on hearing the cry and found your father
fatally injured?
"Witness: Nothing definite.
"The Coroner: What do you mean?
"Witness: I was so disturbed and excited as I rushed out into
the open, that I could think of nothing except of my father. Yet
I have a vague impression that as I ran forward something lay
upon the ground to the left of me. It seemed to me to be
something grey in colour, a coat of some sort, or a plaid perhaps.
When I rose from my father I looked round for it, but it was
gone.
"'Do you mean that it disappeared before you went for help?'
"'Yes, it was gone.'
"'You cannot say what it was?'
"'No, I had a feeling something was there.'
"'How far from the body?'
"'A dozen yards or so.'
"'And how far from the edge of the wood?'
"'About the same.'
"'Then if it was removed it was while you were within a dozen
yards of it?'
"'Yes, but with my back towards it.'
"This concluded the examination of the witness."
"I see," said I as I glanced down the column, "that the coroner
in his concluding remarks was rather severe upon young McCarthy.
He calls attention, and with reason, to the discrepancy about his
father having signalled to him before seeing him, also to his
refusal to give details of his conversation with his father, and
his singular account of his father's dying words. They are all,
as he remarks, very much against the son."
Holmes laughed softly to himself and stretched himself out upon
the cushioned seat. "Both you and the coroner have been at some
pains," said he, "to single out the very strongest points in the
young man's favour. Don't you see that you alternately give him
credit for having too much imagination and too little? Too
little, if he could not invent a cause of quarrel which would
give him the sympathy of the jury; too much, if he evolved from
his own inner consciousness anything so outré as a dying
reference to a rat, and the incident of the vanishing cloth. No,
sir, I shall approach this case from the point of view that what
this young man says is true, and we shall see whither that
hypothesis will lead us. And now here is my pocket Petrarch, and
not another word shall I say of this case until we are on the
scene of action. We lunch at Swindon, and I see that we shall be
there in twenty minutes."
It was nearly four o'clock when we at last, after passing through
the beautiful Stroud Valley, and over the broad gleaming Severn,
found ourselves at the pretty little country-town of Ross. A
lean, ferret-like man, furtive and sly-looking, was waiting for
us upon the platform. In spite of the light brown dustcoat and
leather-leggings which he wore in deference to his rustic
surroundings, I had no difficulty in recognising Lestrade, of
Scotland Yard. With him we drove to the Hereford Arms where a
room had already been engaged for us.
"I have ordered a carriage," said Lestrade as we sat over a cup
of tea. "I knew your energetic nature, and that you would not be
happy until you had been on the scene of the crime."
"It was very nice and complimentary of you," Holmes answered. "It
is entirely a question of barometric pressure."
Lestrade looked startled. "I do not quite follow," he said.
"How is the glass? Twenty-nine, I see. No wind, and not a cloud
in the sky. I have a caseful of cigarettes here which need
smoking, and the sofa is very much superior to the usual country
hotel abomination. I do not think that it is probable that I
shall use the carriage to-night."
Lestrade laughed indulgently. "You have, no doubt, already formed
your conclusions from the newspapers," he said. "The case is as
plain as a pikestaff, and the more one goes into it the plainer
it becomes. Still, of course, one can't refuse a lady, and such a
very positive one, too. She has heard of you, and would have your
opinion, though I repeatedly told her that there was nothing
which you could do which I had not already done. Why, bless my
soul! here is her carriage at the door."
He had hardly spoken before there rushed into the room one of the
most lovely young women that I have ever seen in my life. Her
violet eyes shining, her lips parted, a pink flush upon her
cheeks, all thought of her natural reserve lost in her
overpowering excitement and concern.
"Oh, Mr. Sherlock Holmes!" she cried, glancing from one to the
other of us, and finally, with a woman's quick intuition,
fastening upon my companion, "I am so glad that you have come. I
have driven down to tell you so. I know that James didn't do it.
I know it, and I want you to start upon your work knowing it,
too. Never let yourself doubt upon that point. We have known each
other since we were little children, and I know his faults as no
one else does; but he is too tender-hearted to hurt a fly. Such a
charge is absurd to anyone who really knows him."
"I hope we may clear him, Miss Turner," said Sherlock Holmes.
"You may rely upon my doing all that I can."
"But you have read the evidence. You have formed some conclusion?
Do you not see some loophole, some flaw? Do you not yourself
think that he is innocent?"
"I think that it is very probable."
"There, now!" she cried, throwing back her head and looking
defiantly at Lestrade. "You hear! He gives me hopes."
Lestrade shrugged his shoulders. "I am afraid that my colleague
has been a little quick in forming his conclusions," he said.
"But he is right. Oh! I know that he is right. James never did
it. And about his quarrel with his father, I am sure that the
reason why he would not speak about it to the coroner was because
I was concerned in it."
"In what way?" asked Holmes.
"It is no time for me to hide anything. James and his father had
many disagreements about me. Mr. McCarthy was very anxious that
there should be a marriage between us. James and I have always
loved each other as brother and sister; but of course he is young
and has seen very little of life yet, and--and--well, he
naturally did not wish to do anything like that yet. So there
were quarrels, and this, I am sure, was one of them."
"And your father?" asked Holmes. "Was he in favour of such a
union?"
"No, he was averse to it also. No one but Mr. McCarthy was in
favour of it." A quick blush passed over her fresh young face as
Holmes shot one of his keen, questioning glances at her.
"Thank you for this information," said he. "May I see your father
if I call to-morrow?"
"I am afraid the doctor won't allow it."
"The doctor?"
"Yes, have you not heard? Poor father has never been strong for
years back, but this has broken him down completely. He has taken
to his bed, and Dr. Willows says that he is a wreck and that his
nervous system is shattered. Mr. McCarthy was the only man alive
who had known dad in the old days in Victoria."
"Ha! In Victoria! That is important."
"Yes, at the mines."
"Quite so; at the gold-mines, where, as I understand, Mr. Turner
made his money."
"Yes, certainly."
"Thank you, Miss Turner. You have been of material assistance to
me."
"You will tell me if you have any news to-morrow. No doubt you
will go to the prison to see James. Oh, if you do, Mr. Holmes, do
tell him that I know him to be innocent."
"I will, Miss Turner."
"I must go home now, for dad is very ill, and he misses me so if
I leave him. Good-bye, and God help you in your undertaking." She
hurried from the room as impulsively as she had entered, and we
heard the wheels of her carriage rattle off down the street.
"I am ashamed of you, Holmes," said Lestrade with dignity after a
few minutes' silence. "Why should you raise up hopes which you
are bound to disappoint? I am not over-tender of heart, but I
call it cruel."
"I think that I see my way to clearing James McCarthy," said
Holmes. "Have you an order to see him in prison?"
"Yes, but only for you and me."
"Then I shall reconsider my resolution about going out. We have
still time to take a train to Hereford and see him to-night?"
"Ample."
"Then let us do so. Watson, I fear that you will find it very
slow, but I shall only be away a couple of hours."
I walked down to the station with them, and then wandered through
the streets of the little town, finally returning to the hotel,
where I lay upon the sofa and tried to interest myself in a
yellow-backed novel. The puny plot of the story was so thin,
however, when compared to the deep mystery through which we were
groping, and I found my attention wander so continually from the
action to the fact, that I at last flung it across the room and
gave myself up entirely to a consideration of the events of the
day. Supposing that this unhappy young man's story were
absolutely true, then what hellish thing, what absolutely
unforeseen and extraordinary calamity could have occurred between
the time when he parted from his father, and the moment when,
drawn back by his screams, he rushed into the glade? It was
something terrible and deadly. What could it be? Might not the
nature of the injuries reveal something to my medical instincts?
I rang the bell and called for the weekly county paper, which
contained a verbatim account of the inquest. In the surgeon's
deposition it was stated that the posterior third of the left
parietal bone and the left half of the occipital bone had been
shattered by a heavy blow from a blunt weapon. I marked the spot
upon my own head. Clearly such a blow must have been struck from
behind. That was to some extent in favour of the accused, as when
seen quarrelling he was face to face with his father. Still, it
did not go for very much, for the older man might have turned his
back before the blow fell. Still, it might be worth while to call
Holmes' attention to it. Then there was the peculiar dying
reference to a rat. What could that mean? It could not be
delirium. A man dying from a sudden blow does not commonly become
delirious. No, it was more likely to be an attempt to explain how
he met his fate. But what could it indicate? I cudgelled my
brains to find some possible explanation. And then the incident
of the grey cloth seen by young McCarthy. If that were true the
murderer must have dropped some part of his dress, presumably his
overcoat, in his flight, and must have had the hardihood to
return and to carry it away at the instant when the son was
kneeling with his back turned not a dozen paces off. What a
tissue of mysteries and improbabilities the whole thing was! I
did not wonder at Lestrade's opinion, and yet I had so much faith
in Sherlock Holmes' insight that I could not lose hope as long
as every fresh fact seemed to strengthen his conviction of young
McCarthy's innocence.
It was late before Sherlock Holmes returned. He came back alone,
for Lestrade was staying in lodgings in the town.
"The glass still keeps very high," he remarked as he sat down.
"It is of importance that it should not rain before we are able
to go over the ground. On the other hand, a man should be at his
very best and keenest for such nice work as that, and I did not
wish to do it when fagged by a long journey. I have seen young
McCarthy."
"And what did you learn from him?"
"Nothing."
"Could he throw no light?"
"None at all. I was inclined to think at one time that he knew
who had done it and was screening him or her, but I am convinced
now that he is as puzzled as everyone else. He is not a very
quick-witted youth, though comely to look at and, I should think,
sound at heart."
"I cannot admire his taste," I remarked, "if it is indeed a fact
that he was averse to a marriage with so charming a young lady as
this Miss Turner."
"Ah, thereby hangs a rather painful tale. This fellow is madly,
insanely, in love with her, but some two years ago, when he was
only a lad, and before he really knew her, for she had been away
five years at a boarding-school, what does the idiot do but get
into the clutches of a barmaid in Bristol and marry her at a
registry office? No one knows a word of the matter, but you can
imagine how maddening it must be to him to be upbraided for not
doing what he would give his very eyes to do, but what he knows
to be absolutely impossible. It was sheer frenzy of this sort
which made him throw his hands up into the air when his father,
at their last interview, was goading him on to propose to Miss
Turner. On the other hand, he had no means of supporting himself,
and his father, who was by all accounts a very hard man, would
have thrown him over utterly had he known the truth. It was with
his barmaid wife that he had spent the last three days in
Bristol, and his father did not know where he was. Mark that
point. It is of importance. Good has come out of evil, however,
for the barmaid, finding from the papers that he is in serious
trouble and likely to be hanged, has thrown him over utterly and
has written to him to say that she has a husband already in the
Bermuda Dockyard, so that there is really no tie between them. I
think that that bit of news has consoled young McCarthy for all
that he has suffered."
"But if he is innocent, who has done it?"
"Ah! who? I would call your attention very particularly to two
points. One is that the murdered man had an appointment with
someone at the pool, and that the someone could not have been his
son, for his son was away, and he did not know when he would
return. The second is that the murdered man was heard to cry
'Cooee!' before he knew that his son had returned. Those are the
crucial points upon which the case depends. And now let us talk
about George Meredith, if you please, and we shall leave all
minor matters until to-morrow."
There was no rain, as Holmes had foretold, and the morning broke
bright and cloudless. At nine o'clock Lestrade called for us with
the carriage, and we set off for Hatherley Farm and the Boscombe
Pool.
"There is serious news this morning," Lestrade observed. "It is
said that Mr. Turner, of the Hall, is so ill that his life is
despaired of."
"An elderly man, I presume?" said Holmes.
"About sixty; but his constitution has been shattered by his life
abroad, and he has been in failing health for some time. This
business has had a very bad effect upon him. He was an old friend
of McCarthy's, and, I may add, a great benefactor to him, for I
have learned that he gave him Hatherley Farm rent free."
"Indeed! That is interesting," said Holmes.
"Oh, yes! In a hundred other ways he has helped him. Everybody
about here speaks of his kindness to him."
"Really! Does it not strike you as a little singular that this
McCarthy, who appears to have had little of his own, and to have
been under such obligations to Turner, should still talk of
marrying his son to Turner's daughter, who is, presumably,
heiress to the estate, and that in such a very cocksure manner,
as if it were merely a case of a proposal and all else would
follow? It is the more strange, since we know that Turner himself
was averse to the idea. The daughter told us as much. Do you not
deduce something from that?"
"We have got to the deductions and the inferences," said
Lestrade, winking at me. "I find it hard enough to tackle facts,
Holmes, without flying away after theories and fancies."
"You are right," said Holmes demurely; "you do find it very hard
to tackle the facts."
"Anyhow, I have grasped one fact which you seem to find it
difficult to get hold of," replied Lestrade with some warmth.
"And that is--"
"That McCarthy senior met his death from McCarthy junior and that
all theories to the contrary are the merest moonshine."
"Well, moonshine is a brighter thing than fog," said Holmes,
laughing. "But I am very much mistaken if this is not Hatherley
Farm upon the left."
"Yes, that is it." It was a widespread, comfortable-looking
building, two-storied, slate-roofed, with great yellow blotches
of lichen upon the grey walls. The drawn blinds and the smokeless
chimneys, however, gave it a stricken look, as though the weight
of this horror still lay heavy upon it. We called at the door,
when the maid, at Holmes' request, showed us the boots which her
master wore at the time of his death, and also a pair of the
son's, though not the pair which he had then had. Having measured
these very carefully from seven or eight different points, Holmes
desired to be led to the court-yard, from which we all followed
the winding track which led to Boscombe Pool.
Sherlock Holmes was transformed when he was hot upon such a scent
as this. Men who had only known the quiet thinker and logician of
Baker Street would have failed to recognise him. His face flushed
and darkened. His brows were drawn into two hard black lines,
while his eyes shone out from beneath them with a steely glitter.
His face was bent downward, his shoulders bowed, his lips
compressed, and the veins stood out like whipcord in his long,
sinewy neck. His nostrils seemed to dilate with a purely animal
lust for the chase, and his mind was so absolutely concentrated
upon the matter before him that a question or remark fell
unheeded upon his ears, or, at the most, only provoked a quick,
impatient snarl in reply. Swiftly and silently he made his way
along the track which ran through the meadows, and so by way of
the woods to the Boscombe Pool. It was damp, marshy ground, as is
all that district, and there were marks of many feet, both upon
the path and amid the short grass which bounded it on either
side. Sometimes Holmes would hurry on, sometimes stop dead, and
once he made quite a little detour into the meadow. Lestrade and
I walked behind him, the detective indifferent and contemptuous,
while I watched my friend with the interest which sprang from the
conviction that every one of his actions was directed towards a
definite end.
The Boscombe Pool, which is a little reed-girt sheet of water
some fifty yards across, is situated at the boundary between the
Hatherley Farm and the private park of the wealthy Mr. Turner.
Above the woods which lined it upon the farther side we could see
the red, jutting pinnacles which marked the site of the rich
landowner's dwelling. On the Hatherley side of the pool the woods
grew very thick, and there was a narrow belt of sodden grass
twenty paces across between the edge of the trees and the reeds
which lined the lake. Lestrade showed us the exact spot at which
the body had been found, and, indeed, so moist was the ground,
that I could plainly see the traces which had been left by the
fall of the stricken man. To Holmes, as I could see by his eager
face and peering eyes, very many other things were to be read
upon the trampled grass. He ran round, like a dog who is picking
up a scent, and then turned upon my companion.
"What did you go into the pool for?" he asked.
"I fished about with a rake. I thought there might be some weapon
or other trace. But how on earth--"
"Oh, tut, tut! I have no time! That left foot of yours with its
inward twist is all over the place. A mole could trace it, and
there it vanishes among the reeds. Oh, how simple it would all
have been had I been here before they came like a herd of buffalo
and wallowed all over it. Here is where the party with the
lodge-keeper came, and they have covered all tracks for six or
eight feet round the body. But here are three separate tracks of
the same feet." He drew out a lens and lay down upon his
waterproof to have a better view, talking all the time rather to
himself than to us. "These are young McCarthy's feet. Twice he
was walking, and once he ran swiftly, so that the soles are
deeply marked and the heels hardly visible. That bears out his
story. He ran when he saw his father on the ground. Then here are
the father's feet as he paced up and down. What is this, then? It
is the butt-end of the gun as the son stood listening. And this?
Ha, ha! What have we here? Tiptoes! tiptoes! Square, too, quite
unusual boots! They come, they go, they come again--of course
that was for the cloak. Now where did they come from?" He ran up
and down, sometimes losing, sometimes finding the track until we
were well within the edge of the wood and under the shadow of a
great beech, the largest tree in the neighbourhood. Holmes traced
his way to the farther side of this and lay down once more upon
his face with a little cry of satisfaction. For a long time he
remained there, turning over the leaves and dried sticks,
gathering up what seemed to me to be dust into an envelope and
examining with his lens not only the ground but even the bark of
the tree as far as he could reach. A jagged stone was lying among
the moss, and this also he carefully examined and retained. Then
he followed a pathway through the wood until he came to the
highroad, where all traces were lost.
"It has been a case of considerable interest," he remarked,
returning to his natural manner. "I fancy that this grey house on
the right must be the lodge. I think that I will go in and have a
word with Moran, and perhaps write a little note. Having done
that, we may drive back to our luncheon. You may walk to the cab,
and I shall be with you presently."
It was about ten minutes before we regained our cab and drove
back into Ross, Holmes still carrying with him the stone which he
had picked up in the wood.
"This may interest you, Lestrade," he remarked, holding it out.
"The murder was done with it."
"I see no marks."
"There are none."
"How do you know, then?"
"The grass was growing under it. It had only lain there a few
days. There was no sign of a place whence it had been taken. It
corresponds with the injuries. There is no sign of any other
weapon."
"And the murderer?"
"Is a tall man, left-handed, limps with the right leg, wears
thick-soled shooting-boots and a grey cloak, smokes Indian
cigars, uses a cigar-holder, and carries a blunt pen-knife in his
pocket. There are several other indications, but these may be
enough to aid us in our search."
Lestrade laughed. "I am afraid that I am still a sceptic," he
said. "Theories are all very well, but we have to deal with a
hard-headed British jury."
"Nous verrons," answered Holmes calmly. "You work your own
method, and I shall work mine. I shall be busy this afternoon,
and shall probably return to London by the evening train."
"And leave your case unfinished?"
"No, finished."
"But the mystery?"
"It is solved."
"Who was the criminal, then?"
"The gentleman I describe."
"But who is he?"
"Surely it would not be difficult to find out. This is not such a
populous neighbourhood."
Lestrade shrugged his shoulders. "I am a practical man," he said,
"and I really cannot undertake to go about the country looking
for a left-handed gentleman with a game leg. I should become the
laughing-stock of Scotland Yard."
"All right," said Holmes quietly. "I have given you the chance.
Here are your lodgings. Good-bye. I shall drop you a line before
I leave."
Having left Lestrade at his rooms, we drove to our hotel, where
we found lunch upon the table. Holmes was silent and buried in
thought with a pained expression upon his face, as one who finds
himself in a perplexing position.
"Look here, Watson," he said when the cloth was cleared "just sit
down in this chair and let me preach to you for a little. I don't
know quite what to do, and I should value your advice. Light a
cigar and let me expound."
"Pray do so."
"Well, now, in considering this case there are two points about
young McCarthy's narrative which struck us both instantly,
although they impressed me in his favour and you against him. One
was the fact that his father should, according to his account,
cry 'Cooee!' before seeing him. The other was his singular dying
reference to a rat. He mumbled several words, you understand, but
that was all that caught the son's ear. Now from this double
point our research must commence, and we will begin it by
presuming that what the lad says is absolutely true."
"What of this 'Cooee!' then?"
"Well, obviously it could not have been meant for the son. The
son, as far as he knew, was in Bristol. It was mere chance that
he was within earshot. The 'Cooee!' was meant to attract the
attention of whoever it was that he had the appointment with. But
'Cooee' is a distinctly Australian cry, and one which is used
between Australians. There is a strong presumption that the
person whom McCarthy expected to meet him at Boscombe Pool was
someone who had been in Australia."
"What of the rat, then?"
Sherlock Holmes took a folded paper from his pocket and flattened
it out on the table. "This is a map of the Colony of Victoria,"
he said. "I wired to Bristol for it last night." He put his hand
over part of the map. "What do you read?"
"ARAT," I read.
"And now?" He raised his hand.
"BALLARAT."
"Quite so. That was the word the man uttered, and of which his
son only caught the last two syllables. He was trying to utter
the name of his murderer. So and so, of Ballarat."
"It is wonderful!" I exclaimed.
"It is obvious. And now, you see, I had narrowed the field down
considerably. The possession of a grey garment was a third point
which, granting the son's statement to be correct, was a
certainty. We have come now out of mere vagueness to the definite
conception of an Australian from Ballarat with a grey cloak."
"Certainly."
"And one who was at home in the district, for the pool can only
be approached by the farm or by the estate, where strangers could
hardly wander."
"Quite so."
"Then comes our expedition of to-day. By an examination of the
ground I gained the trifling details which I gave to that
imbecile Lestrade, as to the personality of the criminal."
"But how did you gain them?"
"You know my method. It is founded upon the observation of
trifles."
"His height I know that you might roughly judge from the length
of his stride. His boots, too, might be told from their traces."
"Yes, they were peculiar boots."
"But his lameness?"
"The impression of his right foot was always less distinct than
his left. He put less weight upon it. Why? Because he limped--he
was lame."
"But his left-handedness."
"You were yourself struck by the nature of the injury as recorded
by the surgeon at the inquest. The blow was struck from
immediately behind, and yet was upon the left side. Now, how can
that be unless it were by a left-handed man? He had stood behind
that tree during the interview between the father and son. He had
even smoked there. I found the ash of a cigar, which my special
knowledge of tobacco ashes enables me to pronounce as an Indian
cigar. I have, as you know, devoted some attention to this, and
written a little monograph on the ashes of 140 different
varieties of pipe, cigar, and cigarette tobacco. Having found the
ash, I then looked round and discovered the stump among the moss
where he had tossed it. It was an Indian cigar, of the variety
which are rolled in Rotterdam."
"And the cigar-holder?"
"I could see that the end had not been in his mouth. Therefore he
used a holder. The tip had been cut off, not bitten off, but the
cut was not a clean one, so I deduced a blunt pen-knife."
"Holmes," I said, "you have drawn a net round this man from which
he cannot escape, and you have saved an innocent human life as
truly as if you had cut the cord which was hanging him. I see the
direction in which all this points. The culprit is--"
"Mr. John Turner," cried the hotel waiter, opening the door of
our sitting-room, and ushering in a visitor.
The man who entered was a strange and impressive figure. His
slow, limping step and bowed shoulders gave the appearance of
decrepitude, and yet his hard, deep-lined, craggy features, and
his enormous limbs showed that he was possessed of unusual
strength of body and of character. His tangled beard, grizzled
hair, and outstanding, drooping eyebrows combined to give an air
of dignity and power to his appearance, but his face was of an
ashen white, while his lips and the corners of his nostrils were
tinged with a shade of blue. It was clear to me at a glance that
he was in the grip of some deadly and chronic disease.
"Pray sit down on the sofa," said Holmes gently. "You had my
note?"
"Yes, the lodge-keeper brought it up. You said that you wished to
see me here to avoid scandal."
"I thought people would talk if I went to the Hall."
"And why did you wish to see me?" He looked across at my
companion with despair in his weary eyes, as though his question
was already answered.
"Yes," said Holmes, answering the look rather than the words. "It
is so. I know all about McCarthy."
The old man sank his face in his hands. "God help me!" he cried.
"But I would not have let the young man come to harm. I give you
my word that I would have spoken out if it went against him at
the Assizes."
"I am glad to hear you say so," said Holmes gravely.
"I would have spoken now had it not been for my dear girl. It
would break her heart--it will break her heart when she hears
that I am arrested."
"It may not come to that," said Holmes.
"What?"
"I am no official agent. I understand that it was your daughter
who required my presence here, and I am acting in her interests.
Young McCarthy must be got off, however."
"I am a dying man," said old Turner. "I have had diabetes for
years. My doctor says it is a question whether I shall live a
month. Yet I would rather die under my own roof than in a gaol."
Holmes rose and sat down at the table with his pen in his hand
and a bundle of paper before him. "Just tell us the truth," he
said. "I shall jot down the facts. You will sign it, and Watson
here can witness it. Then I could produce your confession at the
last extremity to save young McCarthy. I promise you that I shall
not use it unless it is absolutely needed."
"It's as well," said the old man; "it's a question whether I
shall live to the Assizes, so it matters little to me, but I
should wish to spare Alice the shock. And now I will make the
thing clear to you; it has been a long time in the acting, but
will not take me long to tell.
"You didn't know this dead man, McCarthy. He was a devil
incarnate. I tell you that. God keep you out of the clutches of
such a man as he. His grip has been upon me these twenty years,
and he has blasted my life. I'll tell you first how I came to be
in his power.
"It was in the early '60's at the diggings. I was a young chap
then, hot-blooded and reckless, ready to turn my hand at
anything; I got among bad companions, took to drink, had no luck
with my claim, took to the bush, and in a word became what you
would call over here a highway robber. There were six of us, and
we had a wild, free life of it, sticking up a station from time
to time, or stopping the wagons on the road to the diggings.
Black Jack of Ballarat was the name I went under, and our party
is still remembered in the colony as the Ballarat Gang.
"One day a gold convoy came down from Ballarat to Melbourne, and
we lay in wait for it and attacked it. There were six troopers
and six of us, so it was a close thing, but we emptied four of
their saddles at the first volley. Three of our boys were killed,
however, before we got the swag. I put my pistol to the head of
the wagon-driver, who was this very man McCarthy. I wish to the
Lord that I had shot him then, but I spared him, though I saw his
wicked little eyes fixed on my face, as though to remember every
feature. We got away with the gold, became wealthy men, and made
our way over to England without being suspected. There I parted
from my old pals and determined to settle down to a quiet and
respectable life. I bought this estate, which chanced to be in
the market, and I set myself to do a little good with my money,
to make up for the way in which I had earned it. I married, too,
and though my wife died young she left me my dear little Alice.
Even when she was just a baby her wee hand seemed to lead me down
the right path as nothing else had ever done. In a word, I turned
over a new leaf and did my best to make up for the past. All was
going well when McCarthy laid his grip upon me.
"I had gone up to town about an investment, and I met him in
Regent Street with hardly a coat to his back or a boot to his
foot.
"'Here we are, Jack,' says he, touching me on the arm; 'we'll be
as good as a family to you. There's two of us, me and my son, and
you can have the keeping of us. If you don't--it's a fine,
law-abiding country is England, and there's always a policeman
within hail.'
"Well, down they came to the west country, there was no shaking
them off, and there they have lived rent free on my best land
ever since. There was no rest for me, no peace, no forgetfulness;
turn where I would, there was his cunning, grinning face at my
elbow. It grew worse as Alice grew up, for he soon saw I was more
afraid of her knowing my past than of the police. Whatever he
wanted he must have, and whatever it was I gave him without
question, land, money, houses, until at last he asked a thing
which I could not give. He asked for Alice.
"His son, you see, had grown up, and so had my girl, and as I was
known to be in weak health, it seemed a fine stroke to him that
his lad should step into the whole property. But there I was
firm. I would not have his cursed stock mixed with mine; not that
I had any dislike to the lad, but his blood was in him, and that
was enough. I stood firm. McCarthy threatened. I braved him to do
his worst. We were to meet at the pool midway between our houses
to talk it over.
"When I went down there I found him talking with his son, so I
smoked a cigar and waited behind a tree until he should be alone.
But as I listened to his talk all that was black and bitter in
me seemed to come uppermost. He was urging his son to marry my
daughter with as little regard for what she might think as if she
were a slut from off the streets. It drove me mad to think that I
and all that I held most dear should be in the power of such a
man as this. Could I not snap the bond? I was already a dying and
a desperate man. Though clear of mind and fairly strong of limb,
I knew that my own fate was sealed. But my memory and my girl!
Both could be saved if I could but silence that foul tongue. I
did it, Mr. Holmes. I would do it again. Deeply as I have sinned,
I have led a life of martyrdom to atone for it. But that my girl
should be entangled in the same meshes which held me was more
than I could suffer. I struck him down with no more compunction
than if he had been some foul and venomous beast. His cry brought
back his son; but I had gained the cover of the wood, though I
was forced to go back to fetch the cloak which I had dropped in
my flight. That is the true story, gentlemen, of all that
occurred."
"Well, it is not for me to judge you," said Holmes as the old man
signed the statement which had been drawn out. "I pray that we
may never be exposed to such a temptation."
"I pray not, sir. And what do you intend to do?"
"In view of your health, nothing. You are yourself aware that you
will soon have to answer for your deed at a higher court than the
Assizes. I will keep your confession, and if McCarthy is
condemned I shall be forced to use it. If not, it shall never be
seen by mortal eye; and your secret, whether you be alive or
dead, shall be safe with us."
"Farewell, then," said the old man solemnly. "Your own deathbeds,
when they come, will be the easier for the thought of the peace
which you have given to mine." Tottering and shaking in all his
giant frame, he stumbled slowly from the room.
"God help us!" said Holmes after a long silence. "Why does fate
play such tricks with poor, helpless worms? I never hear of such
a case as this that I do not think of Baxter's words, and say,
'There, but for the grace of God, goes Sherlock Holmes.'"
James McCarthy was acquitted at the Assizes on the strength of a
number of objections which had been drawn out by Holmes and
submitted to the defending counsel. Old Turner lived for seven
months after our interview, but he is now dead; and there is
every prospect that the son and daughter may come to live happily
together in ignorance of the black cloud which rests upon their
past.
ADVENTURE V. THE FIVE ORANGE PIPS
When I glance over my notes and records of the Sherlock Holmes
cases between the years '82 and '90, I am faced by so many which
present strange and interesting features that it is no easy
matter to know which to choose and which to leave. Some, however,
have already gained publicity through the papers, and others have
not offered a field for those peculiar qualities which my friend
possessed in so high a degree, and which it is the object of
these papers to illustrate. Some, too, have baffled his
analytical skill, and would be, as narratives, beginnings without
an ending, while others have been but partially cleared up, and
have their explanations founded rather upon conjecture and
surmise than on that absolute logical proof which was so dear to
him. There is, however, one of these last which was so remarkable
in its details and so startling in its results that I am tempted
to give some account of it in spite of the fact that there are
points in connection with it which never have been, and probably
never will be, entirely cleared up.
The year '87 furnished us with a long series of cases of greater
or less interest, of which I retain the records. Among my
headings under this one twelve months I find an account of the
adventure of the Paradol Chamber, of the Amateur Mendicant
Society, who held a luxurious club in the lower vault of a
furniture warehouse, of the facts connected with the loss of the
British barque "Sophy Anderson", of the singular adventures of the
Grice Patersons in the island of Uffa, and finally of the
Camberwell poisoning case. In the latter, as may be remembered,
Sherlock Holmes was able, by winding up the dead man's watch, to
prove that it had been wound up two hours before, and that
therefore the deceased had gone to bed within that time--a
deduction which was of the greatest importance in clearing up the
case. All these I may sketch out at some future date, but none of
them present such singular features as the strange train of
circumstances which I have now taken up my pen to describe.
It was in the latter days of September, and the equinoctial gales
had set in with exceptional violence. All day the wind had
screamed and the rain had beaten against the windows, so that
even here in the heart of great, hand-made London we were forced
to raise our minds for the instant from the routine of life and
to recognise the presence of those great elemental forces which
shriek at mankind through the bars of his civilisation, like
untamed beasts in a cage. As evening drew in, the storm grew
higher and louder, and the wind cried and sobbed like a child in
the chimney. Sherlock Holmes sat moodily at one side of the
fireplace cross-indexing his records of crime, while I at the
other was deep in one of Clark Russell's fine sea-stories until
the howl of the gale from without seemed to blend with the text,
and the splash of the rain to lengthen out into the long swash of
the sea waves. My wife was on a visit to her mother's, and for a
few days I was a dweller once more in my old quarters at Baker
Street.
"Why," said I, glancing up at my companion, "that was surely the
bell. Who could come to-night? Some friend of yours, perhaps?"
"Except yourself I have none," he answered. "I do not encourage
visitors."
"A client, then?"
"If so, it is a serious case. Nothing less would bring a man out
on such a day and at such an hour. But I take it that it is more
likely to be some crony of the landlady's."
Sherlock Holmes was wrong in his conjecture, however, for there
came a step in the passage and a tapping at the door. He
stretched out his long arm to turn the lamp away from himself and
towards the vacant chair upon which a newcomer must sit.
"Come in!" said he.
The man who entered was young, some two-and-twenty at the
outside, well-groomed and trimly clad, with something of
refinement and delicacy in his bearing. The streaming umbrella
which he held in his hand, and his long shining waterproof told
of the fierce weather through which he had come. He looked about
him anxiously in the glare of the lamp, and I could see that his
face was pale and his eyes heavy, like those of a man who is
weighed down with some great anxiety.
"I owe you an apology," he said, raising his golden pince-nez to
his eyes. "I trust that I am not intruding. I fear that I have
brought some traces of the storm and rain into your snug
chamber."
"Give me your coat and umbrella," said Holmes. "They may rest
here on the hook and will be dry presently. You have come up from
the south-west, I see."
"Yes, from Horsham."
"That clay and chalk mixture which I see upon your toe caps is
quite distinctive."
"I have come for advice."
"That is easily got."
"And help."
"That is not always so easy."
"I have heard of you, Mr. Holmes. I heard from Major Prendergast
how you saved him in the Tankerville Club scandal."
"Ah, of course. He was wrongfully accused of cheating at cards."
"He said that you could solve anything."
"He said too much."
"That you are never beaten."
"I have been beaten four times--three times by men, and once by a
woman."
"But what is that compared with the number of your successes?"
"It is true that I have been generally successful."
"Then you may be so with me."
"I beg that you will draw your chair up to the fire and favour me
with some details as to your case."
"It is no ordinary one."
"None of those which come to me are. I am the last court of
appeal."
"And yet I question, sir, whether, in all your experience, you
have ever listened to a more mysterious and inexplicable chain of
events than those which have happened in my own family."
"You fill me with interest," said Holmes. "Pray give us the
essential facts from the commencement, and I can afterwards
question you as to those details which seem to me to be most
important."
The young man pulled his chair up and pushed his wet feet out
towards the blaze.
"My name," said he, "is John Openshaw, but my own affairs have,
as far as I can understand, little to do with this awful
business. It is a hereditary matter; so in order to give you an
idea of the facts, I must go back to the commencement of the
affair.
"You must know that my grandfather had two sons--my uncle Elias
and my father Joseph. My father had a small factory at Coventry,
which he enlarged at the time of the invention of bicycling. He
was a patentee of the Openshaw unbreakable tire, and his business
met with such success that he was able to sell it and to retire
upon a handsome competence.
"My uncle Elias emigrated to America when he was a young man and
became a planter in Florida, where he was reported to have done
very well. At the time of the war he fought in Jackson's army,
and afterwards under Hood, where he rose to be a colonel. When
Lee laid down his arms my uncle returned to his plantation, where
he remained for three or four years. About 1869 or 1870 he came
back to Europe and took a small estate in Sussex, near Horsham.
He had made a very considerable fortune in the States, and his
reason for leaving them was his aversion to the negroes, and his
dislike of the Republican policy in extending the franchise to
them. He was a singular man, fierce and quick-tempered, very
foul-mouthed when he was angry, and of a most retiring
disposition. During all the years that he lived at Horsham, I
doubt if ever he set foot in the town. He had a garden and two or
three fields round his house, and there he would take his
exercise, though very often for weeks on end he would never leave
his room. He drank a great deal of brandy and smoked very
heavily, but he would see no society and did not want any
friends, not even his own brother.
"He didn't mind me; in fact, he took a fancy to me, for at the
time when he saw me first I was a youngster of twelve or so. This
would be in the year 1878, after he had been eight or nine years
in England. He begged my father to let me live with him and he
was very kind to me in his way. When he was sober he used to be
fond of playing backgammon and draughts with me, and he would
make me his representative both with the servants and with the
tradespeople, so that by the time that I was sixteen I was quite
master of the house. I kept all the keys and could go where I
liked and do what I liked, so long as I did not disturb him in
his privacy. There was one singular exception, however, for he
had a single room, a lumber-room up among the attics, which was
invariably locked, and which he would never permit either me or
anyone else to enter. With a boy's curiosity I have peeped
through the keyhole, but I was never able to see more than such a
collection of old trunks and bundles as would be expected in such
a room.
"One day--it was in March, 1883--a letter with a foreign stamp
lay upon the table in front of the colonel's plate. It was not a
common thing for him to receive letters, for his bills were all
paid in ready money, and he had no friends of any sort. 'From
India!' said he as he took it up, 'Pondicherry postmark! What can
this be?' Opening it hurriedly, out there jumped five little
dried orange pips, which pattered down upon his plate. I began to
laugh at this, but the laugh was struck from my lips at the sight
of his face. His lip had fallen, his eyes were protruding, his
skin the colour of putty, and he glared at the envelope which he
still held in his trembling hand, 'K. K. K.!' he shrieked, and
then, 'My God, my God, my sins have overtaken me!'
"'What is it, uncle?' I cried.
"'Death,' said he, and rising from the table he retired to his
room, leaving me palpitating with horror. I took up the envelope
and saw scrawled in red ink upon the inner flap, just above the
gum, the letter K three times repeated. There was nothing else
save the five dried pips. What could be the reason of his
overpowering terror? I left the breakfast-table, and as I
ascended the stair I met him coming down with an old rusty key,
which must have belonged to the attic, in one hand, and a small
brass box, like a cashbox, in the other.
"'They may do what they like, but I'll checkmate them still,'
said he with an oath. 'Tell Mary that I shall want a fire in my
room to-day, and send down to Fordham, the Horsham lawyer.'
"I did as he ordered, and when the lawyer arrived I was asked to
step up to the room. The fire was burning brightly, and in the
grate there was a mass of black, fluffy ashes, as of burned
paper, while the brass box stood open and empty beside it. As I
glanced at the box I noticed, with a start, that upon the lid was
printed the treble K which I had read in the morning upon the
envelope.
"'I wish you, John,' said my uncle, 'to witness my will. I leave
my estate, with all its advantages and all its disadvantages, to
my brother, your father, whence it will, no doubt, descend to
you. If you can enjoy it in peace, well and good! If you find you
cannot, take my advice, my boy, and leave it to your deadliest
enemy. I am sorry to give you such a two-edged thing, but I can't
say what turn things are going to take. Kindly sign the paper
where Mr. Fordham shows you.'
"I signed the paper as directed, and the lawyer took it away with
him. The singular incident made, as you may think, the deepest
impression upon me, and I pondered over it and turned it every
way in my mind without being able to make anything of it. Yet I
could not shake off the vague feeling of dread which it left
behind, though the sensation grew less keen as the weeks passed
and nothing happened to disturb the usual routine of our lives. I
could see a change in my uncle, however. He drank more than ever,
and he was less inclined for any sort of society. Most of his
time he would spend in his room, with the door locked upon the
inside, but sometimes he would emerge in a sort of drunken frenzy
and would burst out of the house and tear about the garden with a
revolver in his hand, screaming out that he was afraid of no man,
and that he was not to be cooped up, like a sheep in a pen, by
man or devil. When these hot fits were over, however, he would
rush tumultuously in at the door and lock and bar it behind him,
like a man who can brazen it out no longer against the terror
which lies at the roots of his soul. At such times I have seen
his face, even on a cold day, glisten with moisture, as though it
were new raised from a basin.
"Well, to come to an end of the matter, Mr. Holmes, and not to
abuse your patience, there came a night when he made one of those
drunken sallies from which he never came back. We found him, when
we went to search for him, face downward in a little
green-scummed pool, which lay at the foot of the garden. There
was no sign of any violence, and the water was but two feet deep,
so that the jury, having regard to his known eccentricity,
brought in a verdict of 'suicide.' But I, who knew how he winced
from the very thought of death, had much ado to persuade myself
that he had gone out of his way to meet it. The matter passed,
however, and my father entered into possession of the estate, and
of some 14,000 pounds, which lay to his credit at the bank."
"One moment," Holmes interposed, "your statement is, I foresee,
one of the most remarkable to which I have ever listened. Let me
have the date of the reception by your uncle of the letter, and
the date of his supposed suicide."
"The letter arrived on March 10, 1883. His death was seven weeks
later, upon the night of May 2nd."
"Thank you. Pray proceed."
"When my father took over the Horsham property, he, at my
request, made a careful examination of the attic, which had been
always locked up. We found the brass box there, although its
contents had been destroyed. On the inside of the cover was a
paper label, with the initials of K. K. K. repeated upon it, and
'Letters, memoranda, receipts, and a register' written beneath.
These, we presume, indicated the nature of the papers which had
been destroyed by Colonel Openshaw. For the rest, there was
nothing of much importance in the attic save a great many
scattered papers and note-books bearing upon my uncle's life in
America. Some of them were of the war time and showed that he had
done his duty well and had borne the repute of a brave soldier.
Others were of a date during the reconstruction of the Southern
states, and were mostly concerned with politics, for he had
evidently taken a strong part in opposing the carpet-bag
politicians who had been sent down from the North.
"Well, it was the beginning of '84 when my father came to live at
Horsham, and all went as well as possible with us until the
January of '85. On the fourth day after the new year I heard my
father give a sharp cry of surprise as we sat together at the
breakfast-table. There he was, sitting with a newly opened
envelope in one hand and five dried orange pips in the
outstretched palm of the other one. He had always laughed at what
he called my cock-and-bull story about the colonel, but he looked
very scared and puzzled now that the same thing had come upon
himself.
"'Why, what on earth does this mean, John?' he stammered.
"My heart had turned to lead. 'It is K. K. K.,' said I.
"He looked inside the envelope. 'So it is,' he cried. 'Here are
the very letters. But what is this written above them?'
"'Put the papers on the sundial,' I read, peeping over his
shoulder.
"'What papers? What sundial?' he asked.
"'The sundial in the garden. There is no other,' said I; 'but the
papers must be those that are destroyed.'
"'Pooh!' said he, gripping hard at his courage. 'We are in a
civilised land here, and we can't have tomfoolery of this kind.
Where does the thing come from?'
"'From Dundee,' I answered, glancing at the postmark.
"'Some preposterous practical joke,' said he. 'What have I to do
with sundials and papers? I shall take no notice of such
nonsense.'
"'I should certainly speak to the police,' I said.
"'And be laughed at for my pains. Nothing of the sort.'
"'Then let me do so?'
"'No, I forbid you. I won't have a fuss made about such
nonsense.'
"It was in vain to argue with him, for he was a very obstinate
man. I went about, however, with a heart which was full of
forebodings.
"On the third day after the coming of the letter my father went
from home to visit an old friend of his, Major Freebody, who is
in command of one of the forts upon Portsdown Hill. I was glad
that he should go, for it seemed to me that he was farther from
danger when he was away from home. In that, however, I was in
error. Upon the second day of his absence I received a telegram
from the major, imploring me to come at once. My father had
fallen over one of the deep chalk-pits which abound in the
neighbourhood, and was lying senseless, with a shattered skull. I
hurried to him, but he passed away without having ever recovered
his consciousness. He had, as it appears, been returning from
Fareham in the twilight, and as the country was unknown to him,
and the chalk-pit unfenced, the jury had no hesitation in
bringing in a verdict of 'death from accidental causes.'
Carefully as I examined every fact connected with his death, I
was unable to find anything which could suggest the idea of
murder. There were no signs of violence, no footmarks, no
robbery, no record of strangers having been seen upon the roads.
And yet I need not tell you that my mind was far from at ease,
and that I was well-nigh certain that some foul plot had been
woven round him.
"In this sinister way I came into my inheritance. You will ask me
why I did not dispose of it? I answer, because I was well
convinced that our troubles were in some way dependent upon an
incident in my uncle's life, and that the danger would be as
pressing in one house as in another.
"It was in January, '85, that my poor father met his end, and two
years and eight months have elapsed since then. During that time
I have lived happily at Horsham, and I had begun to hope that
this curse had passed away from the family, and that it had ended
with the last generation. I had begun to take comfort too soon,
however; yesterday morning the blow fell in the very shape in
which it had come upon my father."
The young man took from his waistcoat a crumpled envelope, and
turning to the table he shook out upon it five little dried
orange pips.
"This is the envelope," he continued. "The postmark is
London--eastern division. Within are the very words which were
upon my father's last message: 'K. K. K.'; and then 'Put the
papers on the sundial.'"
"What have you done?" asked Holmes.
"Nothing."
"Nothing?"
"To tell the truth"--he sank his face into his thin, white
hands--"I have felt helpless. I have felt like one of those poor
rabbits when the snake is writhing towards it. I seem to be in
the grasp of some resistless, inexorable evil, which no foresight
and no precautions can guard against."
"Tut! tut!" cried Sherlock Holmes. "You must act, man, or you are
lost. Nothing but energy can save you. This is no time for
despair."
"I have seen the police."
"Ah!"
"But they listened to my story with a smile. I am convinced that
the inspector has formed the opinion that the letters are all
practical jokes, and that the deaths of my relations were really
accidents, as the jury stated, and were not to be connected with
the warnings."
Holmes shook his clenched hands in the air. "Incredible
imbecility!" he cried.
"They have, however, allowed me a policeman, who may remain in
the house with me."
"Has he come with you to-night?"
"No. His orders were to stay in the house."
Again Holmes raved in the air.
"Why did you come to me," he cried, "and, above all, why did you
not come at once?"
"I did not know. It was only to-day that I spoke to Major
Prendergast about my troubles and was advised by him to come to
you."
"It is really two days since you had the letter. We should have
acted before this. You have no further evidence, I suppose, than
that which you have placed before us--no suggestive detail which
might help us?"
"There is one thing," said John Openshaw. He rummaged in his coat
pocket, and, drawing out a piece of discoloured, blue-tinted
paper, he laid it out upon the table. "I have some remembrance,"
said he, "that on the day when my uncle burned the papers I
observed that the small, unburned margins which lay amid the
ashes were of this particular colour. I found this single sheet
upon the floor of his room, and I am inclined to think that it
may be one of the papers which has, perhaps, fluttered out from
among the others, and in that way has escaped destruction. Beyond
the mention of pips, I do not see that it helps us much. I think
myself that it is a page from some private diary. The writing is
undoubtedly my uncle's."
Holmes moved the lamp, and we both bent over the sheet of paper,
which showed by its ragged edge that it had indeed been torn from
a book. It was headed, "March, 1869," and beneath were the
following enigmatical notices:
"4th. Hudson came. Same old platform.
"7th. Set the pips on McCauley, Paramore, and
John Swain, of St. Augustine.
"9th. McCauley cleared.
"10th. John Swain cleared.
"12th. Visited Paramore. All well."
"Thank you!" said Holmes, folding up the paper and returning it
to our visitor. "And now you must on no account lose another
instant. We cannot spare time even to discuss what you have told
me. You must get home instantly and act."
"What shall I do?"
"There is but one thing to do. It must be done at once. You must
put this piece of paper which you have shown us into the brass
box which you have described. You must also put in a note to say
that all the other papers were burned by your uncle, and that
this is the only one which remains. You must assert that in such
words as will carry conviction with them. Having done this, you
must at once put the box out upon the sundial, as directed. Do
you understand?"
"Entirely."
"Do not think of revenge, or anything of the sort, at present. I
think that we may gain that by means of the law; but we have our
web to weave, while theirs is already woven. The first
consideration is to remove the pressing danger which threatens
you. The second is to clear up the mystery and to punish the
guilty parties."
"I thank you," said the young man, rising and pulling on his
overcoat. "You have given me fresh life and hope. I shall
certainly do as you advise."
"Do not lose an instant. And, above all, take care of yourself in
the meanwhile, for I do not think that there can be a doubt that
you are threatened by a very real and imminent danger. How do you
go back?"
"By train from Waterloo."
"It is not yet nine. The streets will be crowded, so I trust that
you may be in safety. And yet you cannot guard yourself too
closely."
"I am armed."
"That is well. To-morrow I shall set to work upon your case."
"I shall see you at Horsham, then?"
"No, your secret lies in London. It is there that I shall seek
it."
"Then I shall call upon you in a day, or in two days, with news
as to the box and the papers. I shall take your advice in every
particular." He shook hands with us and took his leave. Outside
the wind still screamed and the rain splashed and pattered
against the windows. This strange, wild story seemed to have come
to us from amid the mad elements--blown in upon us like a sheet
of sea-weed in a gale--and now to have been reabsorbed by them
once more.
Sherlock Holmes sat for some time in silence, with his head sunk
forward and his eyes bent upon the red glow of the fire. Then he
lit his pipe, and leaning back in his chair he watched the blue
smoke-rings as they chased each other up to the ceiling.
"I think, Watson," he remarked at last, "that of all our cases we
have had none more fantastic than this."
"Save, perhaps, the Sign of Four."
"Well, yes. Save, perhaps, that. And yet this John Openshaw seems
to me to be walking amid even greater perils than did the
Sholtos."
"But have you," I asked, "formed any definite conception as to
what these perils are?"
"There can be no question as to their nature," he answered.
"Then what are they? Who is this K. K. K., and why does he pursue
this unhappy family?"
Sherlock Holmes closed his eyes and placed his elbows upon the
arms of his chair, with his finger-tips together. "The ideal
reasoner," he remarked, "would, when he had once been shown a
single fact in all its bearings, deduce from it not only all the
chain of events which led up to it but also all the results which
would follow from it. As Cuvier could correctly describe a whole
animal by the contemplation of a single bone, so the observer who
has thoroughly understood one link in a series of incidents
should be able to accurately state all the other ones, both
before and after. We have not yet grasped the results which the
reason alone can attain to. Problems may be solved in the study
which have baffled all those who have sought a solution by the
aid of their senses. To carry the art, however, to its highest
pitch, it is necessary that the reasoner should be able to
utilise all the facts which have come to his knowledge; and this
in itself implies, as you will readily see, a possession of all
knowledge, which, even in these days of free education and
encyclopaedias, is a somewhat rare accomplishment. It is not so
impossible, however, that a man should possess all knowledge
which is likely to be useful to him in his work, and this I have
endeavoured in my case to do. If I remember rightly, you on one
occasion, in the early days of our friendship, defined my limits
in a very precise fashion."
"Yes," I answered, laughing. "It was a singular document.
Philosophy, astronomy, and politics were marked at zero, I
remember. Botany variable, geology profound as regards the
mud-stains from any region within fifty miles of town, chemistry
eccentric, anatomy unsystematic, sensational literature and crime
records unique, violin-player, boxer, swordsman, lawyer, and
self-poisoner by cocaine and tobacco. Those, I think, were the
main points of my analysis."
Holmes grinned at the last item. "Well," he said, "I say now, as
I said then, that a man should keep his little brain-attic
stocked with all the furniture that he is likely to use, and the
rest he can put away in the lumber-room of his library, where he
can get it if he wants it. Now, for such a case as the one which
has been submitted to us to-night, we need certainly to muster
all our resources. Kindly hand me down the letter K of the
'American Encyclopaedia' which stands upon the shelf beside you.
Thank you. Now let us consider the situation and see what may be
deduced from it. In the first place, we may start with a strong
presumption that Colonel Openshaw had some very strong reason for
leaving America. Men at his time of life do not change all their
habits and exchange willingly the charming climate of Florida for
the lonely life of an English provincial town. His extreme love
of solitude in England suggests the idea that he was in fear of
someone or something, so we may assume as a working hypothesis
that it was fear of someone or something which drove him from
America. As to what it was he feared, we can only deduce that by
considering the formidable letters which were received by himself
and his successors. Did you remark the postmarks of those
letters?"
"The first was from Pondicherry, the second from Dundee, and the
third from London."
"From East London. What do you deduce from that?"
"They are all seaports. That the writer was on board of a ship."
"Excellent. We have already a clue. There can be no doubt that
the probability--the strong probability--is that the writer was
on board of a ship. And now let us consider another point. In the
case of Pondicherry, seven weeks elapsed between the threat and
its fulfilment, in Dundee it was only some three or four days.
Does that suggest anything?"
"A greater distance to travel."
"But the letter had also a greater distance to come."
"Then I do not see the point."
"There is at least a presumption that the vessel in which the man
or men are is a sailing-ship. It looks as if they always send
their singular warning or token before them when starting upon
their mission. You see how quickly the deed followed the sign
when it came from Dundee. If they had come from Pondicherry in a
steamer they would have arrived almost as soon as their letter.
But, as a matter of fact, seven weeks elapsed. I think that those
seven weeks represented the difference between the mail-boat which
brought the letter and the sailing vessel which brought the
writer."
"It is possible."
"More than that. It is probable. And now you see the deadly
urgency of this new case, and why I urged young Openshaw to
caution. The blow has always fallen at the end of the time which
it would take the senders to travel the distance. But this one
comes from London, and therefore we cannot count upon delay."
"Good God!" I cried. "What can it mean, this relentless
persecution?"
"The papers which Openshaw carried are obviously of vital
importance to the person or persons in the sailing-ship. I think
that it is quite clear that there must be more than one of them.
A single man could not have carried out two deaths in such a way
as to deceive a coroner's jury. There must have been several in
it, and they must have been men of resource and determination.
Their papers they mean to have, be the holder of them who it may.
In this way you see K. K. K. ceases to be the initials of an
individual and becomes the badge of a society."
"But of what society?"
"Have you never--" said Sherlock Holmes, bending forward and
sinking his voice--"have you never heard of the Ku Klux Klan?"
"I never have."
Holmes turned over the leaves of the book upon his knee. "Here it
is," said he presently:
"'Ku Klux Klan. A name derived from the fanciful resemblance to
the sound produced by cocking a rifle. This terrible secret
society was formed by some ex-Confederate soldiers in the
Southern states after the Civil War, and it rapidly formed local
branches in different parts of the country, notably in Tennessee,
Louisiana, the Carolinas, Georgia, and Florida. Its power was
used for political purposes, principally for the terrorising of
the negro voters and the murdering and driving from the country
of those who were opposed to its views. Its outrages were usually
preceded by a warning sent to the marked man in some fantastic
but generally recognised shape--a sprig of oak-leaves in some
parts, melon seeds or orange pips in others. On receiving this
the victim might either openly abjure his former ways, or might
fly from the country. If he braved the matter out, death would
unfailingly come upon him, and usually in some strange and
unforeseen manner. So perfect was the organisation of the
society, and so systematic its methods, that there is hardly a
case upon record where any man succeeded in braving it with
impunity, or in which any of its outrages were traced home to the
perpetrators. For some years the organisation flourished in spite
of the efforts of the United States government and of the better
classes of the community in the South. Eventually, in the year
1869, the movement rather suddenly collapsed, although there have
been sporadic outbreaks of the same sort since that date.'
"You will observe," said Holmes, laying down the volume, "that
the sudden breaking up of the society was coincident with the
disappearance of Openshaw from America with their papers. It may
well have been cause and effect. It is no wonder that he and his
family have some of the more implacable spirits upon their track.
You can understand that this register and diary may implicate
some of the first men in the South, and that there may be many
who will not sleep easy at night until it is recovered."
"Then the page we have seen--"
"Is such as we might expect. It ran, if I remember right, 'sent
the pips to A, B, and C'--that is, sent the society's warning to
them. Then there are successive entries that A and B cleared, or
left the country, and finally that C was visited, with, I fear, a
sinister result for C. Well, I think, Doctor, that we may let
some light into this dark place, and I believe that the only
chance young Openshaw has in the meantime is to do what I have
told him. There is nothing more to be said or to be done
to-night, so hand me over my violin and let us try to forget for
half an hour the miserable weather and the still more miserable
ways of our fellow-men."
It had cleared in the morning, and the sun was shining with a
subdued brightness through the dim veil which hangs over the
great city. Sherlock Holmes was already at breakfast when I came
down.
"You will excuse me for not waiting for you," said he; "I have, I
foresee, a very busy day before me in looking into this case of
young Openshaw's."
"What steps will you take?" I asked.
"It will very much depend upon the results of my first inquiries.
I may have to go down to Horsham, after all."
"You will not go there first?"
"No, I shall commence with the City. Just ring the bell and the
maid will bring up your coffee."
As I waited, I lifted the unopened newspaper from the table and
glanced my eye over it. It rested upon a heading which sent a
chill to my heart.
"Holmes," I cried, "you are too late."
"Ah!" said he, laying down his cup, "I feared as much. How was it
done?" He spoke calmly, but I could see that he was deeply moved.
"My eye caught the name of Openshaw, and the heading 'Tragedy
Near Waterloo Bridge.' Here is the account:
"Between nine and ten last night Police-Constable Cook, of the H
Division, on duty near Waterloo Bridge, heard a cry for help and
a splash in the water. The night, however, was extremely dark and
stormy, so that, in spite of the help of several passers-by, it
was quite impossible to effect a rescue. The alarm, however, was
given, and, by the aid of the water-police, the body was
eventually recovered. It proved to be that of a young gentleman
whose name, as it appears from an envelope which was found in his
pocket, was John Openshaw, and whose residence is near Horsham.
It is conjectured that he may have been hurrying down to catch
the last train from Waterloo Station, and that in his haste and
the extreme darkness he missed his path and walked over the edge
of one of the small landing-places for river steamboats. The body
exhibited no traces of violence, and there can be no doubt that
the deceased had been the victim of an unfortunate accident,
which should have the effect of calling the attention of the
authorities to the condition of the riverside landing-stages."
We sat in silence for some minutes, Holmes more depressed and
shaken than I had ever seen him.
"That hurts my pride, Watson," he said at last. "It is a petty
feeling, no doubt, but it hurts my pride. It becomes a personal
matter with me now, and, if God sends me health, I shall set my
hand upon this gang. That he should come to me for help, and that
I should send him away to his death--!" He sprang from his chair
and paced about the room in uncontrollable agitation, with a
flush upon his sallow cheeks and a nervous clasping and
unclasping of his long thin hands.
"They must be cunning devils," he exclaimed at last. "How could
they have decoyed him down there? The Embankment is not on the
direct line to the station. The bridge, no doubt, was too
crowded, even on such a night, for their purpose. Well, Watson,
we shall see who will win in the long run. I am going out now!"
"To the police?"
"No; I shall be my own police. When I have spun the web they may
take the flies, but not before."
All day I was engaged in my professional work, and it was late in
the evening before I returned to Baker Street. Sherlock Holmes
had not come back yet. It was nearly ten o'clock before he
entered, looking pale and worn. He walked up to the sideboard,
and tearing a piece from the loaf he devoured it voraciously,
washing it down with a long draught of water.
"You are hungry," I remarked.
"Starving. It had escaped my memory. I have had nothing since
breakfast."
"Nothing?"
"Not a bite. I had no time to think of it."
"And how have you succeeded?"
"Well."
"You have a clue?"
"I have them in the hollow of my hand. Young Openshaw shall not
long remain unavenged. Why, Watson, let us put their own devilish
trade-mark upon them. It is well thought of!"
"What do you mean?"
He took an orange from the cupboard, and tearing it to pieces he
squeezed out the pips upon the table. Of these he took five and
thrust them into an envelope. On the inside of the flap he wrote
"S. H. for J. O." Then he sealed it and addressed it to "Captain
James Calhoun, Barque 'Lone Star,' Savannah, Georgia."
"That will await him when he enters port," said he, chuckling.
"It may give him a sleepless night. He will find it as sure a
precursor of his fate as Openshaw did before him."
"And who is this Captain Calhoun?"
"The leader of the gang. I shall have the others, but he first."
"How did you trace it, then?"
He took a large sheet of paper from his pocket, all covered with
dates and names.
"I have spent the whole day," said he, "over Lloyd's registers
and files of the old papers, following the future career of every
vessel which touched at Pondicherry in January and February in
'83. There were thirty-six ships of fair tonnage which were
reported there during those months. Of these, one, the 'Lone Star,'
instantly attracted my attention, since, although it was reported
as having cleared from London, the name is that which is given to
one of the states of the Union."
"Texas, I think."
"I was not and am not sure which; but I knew that the ship must
have an American origin."
"What then?"
"I searched the Dundee records, and when I found that the barque
'Lone Star' was there in January, '85, my suspicion became a
certainty. I then inquired as to the vessels which lay at present
in the port of London."
"Yes?"
"The 'Lone Star' had arrived here last week. I went down to the
Albert Dock and found that she had been taken down the river by
the early tide this morning, homeward bound to Savannah. I wired
to Gravesend and learned that she had passed some time ago, and
as the wind is easterly I have no doubt that she is now past the
Goodwins and not very far from the Isle of Wight."
"What will you do, then?"
"Oh, I have my hand upon him. He and the two mates, are as I
learn, the only native-born Americans in the ship. The others are
Finns and Germans. I know, also, that they were all three away
from the ship last night. I had it from the stevedore who has
been loading their cargo. By the time that their sailing-ship
reaches Savannah the mail-boat will have carried this letter, and
the cable will have informed the police of Savannah that these
three gentlemen are badly wanted here upon a charge of murder."
There is ever a flaw, however, in the best laid of human plans,
and the murderers of John Openshaw were never to receive the
orange pips which would show them that another, as cunning and as
resolute as themselves, was upon their track. Very long and very
severe were the equinoctial gales that year. We waited long for
news of the "Lone Star" of Savannah, but none ever reached us. We
did at last hear that somewhere far out in the Atlantic a
shattered stern-post of a boat was seen swinging in the trough
of a wave, with the letters "L. S." carved upon it, and that is
all which we shall ever know of the fate of the "Lone Star."
ADVENTURE VI. THE MAN WITH THE TWISTED LIP
Isa Whitney, brother of the late Elias Whitney, D.D., Principal
of the Theological College of St. George's, was much addicted to
opium. The habit grew upon him, as I understand, from some
foolish freak when he was at college; for having read De
Quincey's description of his dreams and sensations, he had
drenched his tobacco with laudanum in an attempt to produce the
same effects. He found, as so many more have done, that the
practice is easier to attain than to get rid of, and for many
years he continued to be a slave to the drug, an object of
mingled horror and pity to his friends and relatives. I can see
him now, with yellow, pasty face, drooping lids, and pin-point
pupils, all huddled in a chair, the wreck and ruin of a noble
man.
One night--it was in June, '89--there came a ring to my bell,
about the hour when a man gives his first yawn and glances at the
clock. I sat up in my chair, and my wife laid her needle-work
down in her lap and made a little face of disappointment.
"A patient!" said she. "You'll have to go out."
I groaned, for I was newly come back from a weary day.
We heard the door open, a few hurried words, and then quick steps
upon the linoleum. Our own door flew open, and a lady, clad in
some dark-coloured stuff, with a black veil, entered the room.
"You will excuse my calling so late," she began, and then,
suddenly losing her self-control, she ran forward, threw her arms
about my wife's neck, and sobbed upon her shoulder. "Oh, I'm in
such trouble!" she cried; "I do so want a little help."
"Why," said my wife, pulling up her veil, "it is Kate Whitney.
How you startled me, Kate! I had not an idea who you were when
you came in."
"I didn't know what to do, so I came straight to you." That was
always the way. Folk who were in grief came to my wife like birds
to a light-house.
"It was very sweet of you to come. Now, you must have some wine
and water, and sit here comfortably and tell us all about it. Or
should you rather that I sent James off to bed?"
"Oh, no, no! I want the doctor's advice and help, too. It's about
Isa. He has not been home for two days. I am so frightened about
him!"
It was not the first time that she had spoken to us of her
husband's trouble, to me as a doctor, to my wife as an old friend
and school companion. We soothed and comforted her by such words
as we could find. Did she know where her husband was? Was it
possible that we could bring him back to her?
It seems that it was. She had the surest information that of late
he had, when the fit was on him, made use of an opium den in the
farthest east of the City. Hitherto his orgies had always been
confined to one day, and he had come back, twitching and
shattered, in the evening. But now the spell had been upon him
eight-and-forty hours, and he lay there, doubtless among the
dregs of the docks, breathing in the poison or sleeping off the
effects. There he was to be found, she was sure of it, at the Bar
of Gold, in Upper Swandam Lane. But what was she to do? How could
she, a young and timid woman, make her way into such a place and
pluck her husband out from among the ruffians who surrounded him?
There was the case, and of course there was but one way out of
it. Might I not escort her to this place? And then, as a second
thought, why should she come at all? I was Isa Whitney's medical
adviser, and as such I had influence over him. I could manage it
better if I were alone. I promised her on my word that I would
send him home in a cab within two hours if he were indeed at the
address which she had given me. And so in ten minutes I had left
my armchair and cheery sitting-room behind me, and was speeding
eastward in a hansom on a strange errand, as it seemed to me at
the time, though the future only could show how strange it was to
be.
But there was no great difficulty in the first stage of my
adventure. Upper Swandam Lane is a vile alley lurking behind the
high wharves which line the north side of the river to the east
of London Bridge. Between a slop-shop and a gin-shop, approached
by a steep flight of steps leading down to a black gap like the
mouth of a cave, I found the den of which I was in search.
Ordering my cab to wait, I passed down the steps, worn hollow in
the centre by the ceaseless tread of drunken feet; and by the
light of a flickering oil-lamp above the door I found the latch
and made my way into a long, low room, thick and heavy with the
brown opium smoke, and terraced with wooden berths, like the
forecastle of an emigrant ship.
Through the gloom one could dimly catch a glimpse of bodies lying
in strange fantastic poses, bowed shoulders, bent knees, heads
thrown back, and chins pointing upward, with here and there a
dark, lack-lustre eye turned upon the newcomer. Out of the black
shadows there glimmered little red circles of light, now bright,
now faint, as the burning poison waxed or waned in the bowls of
the metal pipes. The most lay silent, but some muttered to
themselves, and others talked together in a strange, low,
monotonous voice, their conversation coming in gushes, and then
suddenly tailing off into silence, each mumbling out his own
thoughts and paying little heed to the words of his neighbour. At
the farther end was a small brazier of burning charcoal, beside
which on a three-legged wooden stool there sat a tall, thin old
man, with his jaw resting upon his two fists, and his elbows upon
his knees, staring into the fire.
As I entered, a sallow Malay attendant had hurried up with a pipe
for me and a supply of the drug, beckoning me to an empty berth.
"Thank you. I have not come to stay," said I. "There is a friend
of mine here, Mr. Isa Whitney, and I wish to speak with him."
There was a movement and an exclamation from my right, and
peering through the gloom, I saw Whitney, pale, haggard, and
unkempt, staring out at me.
"My God! It's Watson," said he. He was in a pitiable state of
reaction, with every nerve in a twitter. "I say, Watson, what
o'clock is it?"
"Nearly eleven."
"Of what day?"
"Of Friday, June 19th."
"Good heavens! I thought it was Wednesday. It is Wednesday. What
d'you want to frighten a chap for?" He sank his face onto his
arms and began to sob in a high treble key.
"I tell you that it is Friday, man. Your wife has been waiting
this two days for you. You should be ashamed of yourself!"
"So I am. But you've got mixed, Watson, for I have only been here
a few hours, three pipes, four pipes--I forget how many. But I'll
go home with you. I wouldn't frighten Kate--poor little Kate.
Give me your hand! Have you a cab?"
"Yes, I have one waiting."
"Then I shall go in it. But I must owe something. Find what I
owe, Watson. I am all off colour. I can do nothing for myself."
I walked down the narrow passage between the double row of
sleepers, holding my breath to keep out the vile, stupefying
fumes of the drug, and looking about for the manager. As I passed
the tall man who sat by the brazier I felt a sudden pluck at my
skirt, and a low voice whispered, "Walk past me, and then look
back at me." The words fell quite distinctly upon my ear. I
glanced down. They could only have come from the old man at my
side, and yet he sat now as absorbed as ever, very thin, very
wrinkled, bent with age, an opium pipe dangling down from between
his knees, as though it had dropped in sheer lassitude from his
fingers. I took two steps forward and looked back. It took all my
self-control to prevent me from breaking out into a cry of
astonishment. He had turned his back so that none could see him
but I. His form had filled out, his wrinkles were gone, the dull
eyes had regained their fire, and there, sitting by the fire and
grinning at my surprise, was none other than Sherlock Holmes. He
made a slight motion to me to approach him, and instantly, as he
turned his face half round to the company once more, subsided
into a doddering, loose-lipped senility.
"Holmes!" I whispered, "what on earth are you doing in this den?"
"As low as you can," he answered; "I have excellent ears. If you
would have the great kindness to get rid of that sottish friend
of yours I should be exceedingly glad to have a little talk with
you."
"I have a cab outside."
"Then pray send him home in it. You may safely trust him, for he
appears to be too limp to get into any mischief. I should
recommend you also to send a note by the cabman to your wife to
say that you have thrown in your lot with me. If you will wait
outside, I shall be with you in five minutes."
It was difficult to refuse any of Sherlock Holmes' requests, for
they were always so exceedingly definite, and put forward with
such a quiet air of mastery. I felt, however, that when Whitney
was once confined in the cab my mission was practically
accomplished; and for the rest, I could not wish anything better
than to be associated with my friend in one of those singular
adventures which were the normal condition of his existence. In a
few minutes I had written my note, paid Whitney's bill, led him
out to the cab, and seen him driven through the darkness. In a
very short time a decrepit figure had emerged from the opium den,
and I was walking down the street with Sherlock Holmes. For two
streets he shuffled along with a bent back and an uncertain foot.
Then, glancing quickly round, he straightened himself out and
burst into a hearty fit of laughter.
"I suppose, Watson," said he, "that you imagine that I have added
opium-smoking to cocaine injections, and all the other little
weaknesses on which you have favoured me with your medical
views."
"I was certainly surprised to find you there."
"But not more so than I to find you."
"I came to find a friend."
"And I to find an enemy."
"An enemy?"
"Yes; one of my natural enemies, or, shall I say, my natural
prey. Briefly, Watson, I am in the midst of a very remarkable
inquiry, and I have hoped to find a clue in the incoherent
ramblings of these sots, as I have done before now. Had I been
recognised in that den my life would not have been worth an
hour's purchase; for I have used it before now for my own
purposes, and the rascally Lascar who runs it has sworn to have
vengeance upon me. There is a trap-door at the back of that
building, near the corner of Paul's Wharf, which could tell some
strange tales of what has passed through it upon the moonless
nights."
"What! You do not mean bodies?"
"Ay, bodies, Watson. We should be rich men if we had 1000 pounds
for every poor devil who has been done to death in that den. It
is the vilest murder-trap on the whole riverside, and I fear that
Neville St. Clair has entered it never to leave it more. But our
trap should be here." He put his two forefingers between his
teeth and whistled shrilly--a signal which was answered by a
similar whistle from the distance, followed shortly by the rattle
of wheels and the clink of horses' hoofs.
"Now, Watson," said Holmes, as a tall dog-cart dashed up through
the gloom, throwing out two golden tunnels of yellow light from
its side lanterns. "You'll come with me, won't you?"
"If I can be of use."
"Oh, a trusty comrade is always of use; and a chronicler still
more so. My room at The Cedars is a double-bedded one."
"The Cedars?"
"Yes; that is Mr. St. Clair's house. I am staying there while I
conduct the inquiry."
"Where is it, then?"
"Near Lee, in Kent. We have a seven-mile drive before us."
"But I am all in the dark."
"Of course you are. You'll know all about it presently. Jump up
here. All right, John; we shall not need you. Here's half a
crown. Look out for me to-morrow, about eleven. Give her her
head. So long, then!"
He flicked the horse with his whip, and we dashed away through
the endless succession of sombre and deserted streets, which
widened gradually, until we were flying across a broad
balustraded bridge, with the murky river flowing sluggishly
beneath us. Beyond lay another dull wilderness of bricks and
mortar, its silence broken only by the heavy, regular footfall of
the policeman, or the songs and shouts of some belated party of
revellers. A dull wrack was drifting slowly across the sky, and a
star or two twinkled dimly here and there through the rifts of
the clouds. Holmes drove in silence, with his head sunk upon his
breast, and the air of a man who is lost in thought, while I sat
beside him, curious to learn what this new quest might be which
seemed to tax his powers so sorely, and yet afraid to break in
upon the current of his thoughts. We had driven several miles,
and were beginning to get to the fringe of the belt of suburban
villas, when he shook himself, shrugged his shoulders, and lit up
his pipe with the air of a man who has satisfied himself that he
is acting for the best.
"You have a grand gift of silence, Watson," said he. "It makes
you quite invaluable as a companion. 'Pon my word, it is a great
thing for me to have someone to talk to, for my own thoughts are
not over-pleasant. I was wondering what I should say to this dear
little woman to-night when she meets me at the door."
"You forget that I know nothing about it."
"I shall just have time to tell you the facts of the case before
we get to Lee. It seems absurdly simple, and yet, somehow I can
get nothing to go upon. There's plenty of thread, no doubt, but I
can't get the end of it into my hand. Now, I'll state the case
clearly and concisely to you, Watson, and maybe you can see a
spark where all is dark to me."
"Proceed, then."
"Some years ago--to be definite, in May, 1884--there came to Lee
a gentleman, Neville St. Clair by name, who appeared to have
plenty of money. He took a large villa, laid out the grounds very
nicely, and lived generally in good style. By degrees he made
friends in the neighbourhood, and in 1887 he married the daughter
of a local brewer, by whom he now has two children. He had no
occupation, but was interested in several companies and went into
town as a rule in the morning, returning by the 5:14 from Cannon
Street every night. Mr. St. Clair is now thirty-seven years of
age, is a man of temperate habits, a good husband, a very
affectionate father, and a man who is popular with all who know
him. I may add that his whole debts at the present moment, as far
as we have been able to ascertain, amount to 88 pounds 10s., while
he has 220 pounds standing to his credit in the Capital and
Counties Bank. There is no reason, therefore, to think that money
troubles have been weighing upon his mind.
"Last Monday Mr. Neville St. Clair went into town rather earlier
than usual, remarking before he started that he had two important
commissions to perform, and that he would bring his little boy
home a box of bricks. Now, by the merest chance, his wife
received a telegram upon this same Monday, very shortly after his
departure, to the effect that a small parcel of considerable
value which she had been expecting was waiting for her at the
offices of the Aberdeen Shipping Company. Now, if you are well up
in your London, you will know that the office of the company is
in Fresno Street, which branches out of Upper Swandam Lane, where
you found me to-night. Mrs. St. Clair had her lunch, started for
the City, did some shopping, proceeded to the company's office,
got her packet, and found herself at exactly 4:35 walking through
Swandam Lane on her way back to the station. Have you followed me
so far?"
"It is very clear."
"If you remember, Monday was an exceedingly hot day, and Mrs. St.
Clair walked slowly, glancing about in the hope of seeing a cab,
as she did not like the neighbourhood in which she found herself.
While she was walking in this way down Swandam Lane, she suddenly
heard an ejaculation or cry, and was struck cold to see her
husband looking down at her and, as it seemed to her, beckoning
to her from a second-floor window. The window was open, and she
distinctly saw his face, which she describes as being terribly
agitated. He waved his hands frantically to her, and then
vanished from the window so suddenly that it seemed to her that
he had been plucked back by some irresistible force from behind.
One singular point which struck her quick feminine eye was that
although he wore some dark coat, such as he had started to town
in, he had on neither collar nor necktie.
"Convinced that something was amiss with him, she rushed down the
steps--for the house was none other than the opium den in which
you found me to-night--and running through the front room she
attempted to ascend the stairs which led to the first floor. At
the foot of the stairs, however, she met this Lascar scoundrel of
whom I have spoken, who thrust her back and, aided by a Dane, who
acts as assistant there, pushed her out into the street. Filled
with the most maddening doubts and fears, she rushed down the
lane and, by rare good-fortune, met in Fresno Street a number of
constables with an inspector, all on their way to their beat. The
inspector and two men accompanied her back, and in spite of the
continued resistance of the proprietor, they made their way to
the room in which Mr. St. Clair had last been seen. There was no
sign of him there. In fact, in the whole of that floor there was
no one to be found save a crippled wretch of hideous aspect, who,
it seems, made his home there. Both he and the Lascar stoutly
swore that no one else had been in the front room during the
afternoon. So determined was their denial that the inspector was
staggered, and had almost come to believe that Mrs. St. Clair had
been deluded when, with a cry, she sprang at a small deal box
which lay upon the table and tore the lid from it. Out there fell
a cascade of children's bricks. It was the toy which he had
promised to bring home.
"This discovery, and the evident confusion which the cripple
showed, made the inspector realise that the matter was serious.
The rooms were carefully examined, and results all pointed to an
abominable crime. The front room was plainly furnished as a
sitting-room and led into a small bedroom, which looked out upon
the back of one of the wharves. Between the wharf and the bedroom
window is a narrow strip, which is dry at low tide but is covered
at high tide with at least four and a half feet of water. The
bedroom window was a broad one and opened from below. On
examination traces of blood were to be seen upon the windowsill,
and several scattered drops were visible upon the wooden floor of
the bedroom. Thrust away behind a curtain in the front room were
all the clothes of Mr. Neville St. Clair, with the exception of
his coat. His boots, his socks, his hat, and his watch--all were
there. There were no signs of violence upon any of these
garments, and there were no other traces of Mr. Neville St.
Clair. Out of the window he must apparently have gone for no
other exit could be discovered, and the ominous bloodstains upon
the sill gave little promise that he could save himself by
swimming, for the tide was at its very highest at the moment of
the tragedy.
"And now as to the villains who seemed to be immediately
implicated in the matter. The Lascar was known to be a man of the
vilest antecedents, but as, by Mrs. St. Clair's story, he was
known to have been at the foot of the stair within a very few
seconds of her husband's appearance at the window, he could
hardly have been more than an accessory to the crime. His defence
was one of absolute ignorance, and he protested that he had no
knowledge as to the doings of Hugh Boone, his lodger, and that he
could not account in any way for the presence of the missing
gentleman's clothes.
"So much for the Lascar manager. Now for the sinister cripple who
lives upon the second floor of the opium den, and who was
certainly the last human being whose eyes rested upon Neville St.
Clair. His name is Hugh Boone, and his hideous face is one which
is familiar to every man who goes much to the City. He is a
professional beggar, though in order to avoid the police
regulations he pretends to a small trade in wax vestas. Some
little distance down Threadneedle Street, upon the left-hand
side, there is, as you may have remarked, a small angle in the
wall. Here it is that this creature takes his daily seat,
cross-legged with his tiny stock of matches on his lap, and as he
is a piteous spectacle a small rain of charity descends into the
greasy leather cap which lies upon the pavement beside him. I
have watched the fellow more than once before ever I thought of
making his professional acquaintance, and I have been surprised
at the harvest which he has reaped in a short time. His
appearance, you see, is so remarkable that no one can pass him
without observing him. A shock of orange hair, a pale face
disfigured by a horrible scar, which, by its contraction, has
turned up the outer edge of his upper lip, a bulldog chin, and a
pair of very penetrating dark eyes, which present a singular
contrast to the colour of his hair, all mark him out from amid
the common crowd of mendicants and so, too, does his wit, for he
is ever ready with a reply to any piece of chaff which may be
thrown at him by the passers-by. This is the man whom we now
learn to have been the lodger at the opium den, and to have been
the last man to see the gentleman of whom we are in quest."
"But a cripple!" said I. "What could he have done single-handed
against a man in the prime of life?"
"He is a cripple in the sense that he walks with a limp; but in
other respects he appears to be a powerful and well-nurtured man.
Surely your medical experience would tell you, Watson, that
weakness in one limb is often compensated for by exceptional
strength in the others."
"Pray continue your narrative."
"Mrs. St. Clair had fainted at the sight of the blood upon the
window, and she was escorted home in a cab by the police, as her
presence could be of no help to them in their investigations.
Inspector Barton, who had charge of the case, made a very careful
examination of the premises, but without finding anything which
threw any light upon the matter. One mistake had been made in not
arresting Boone instantly, as he was allowed some few minutes
during which he might have communicated with his friend the
Lascar, but this fault was soon remedied, and he was seized and
searched, without anything being found which could incriminate
him. There were, it is true, some blood-stains upon his right
shirt-sleeve, but he pointed to his ring-finger, which had been
cut near the nail, and explained that the bleeding came from
there, adding that he had been to the window not long before, and
that the stains which had been observed there came doubtless from
the same source. He denied strenuously having ever seen Mr.
Neville St. Clair and swore that the presence of the clothes in
his room was as much a mystery to him as to the police. As to
Mrs. St. Clair's assertion that she had actually seen her husband
at the window, he declared that she must have been either mad or
dreaming. He was removed, loudly protesting, to the
police-station, while the inspector remained upon the premises in
the hope that the ebbing tide might afford some fresh clue.
"And it did, though they hardly found upon the mud-bank what they
had feared to find. It was Neville St. Clair's coat, and not
Neville St. Clair, which lay uncovered as the tide receded. And
what do you think they found in the pockets?"
"I cannot imagine."
"No, I don't think you would guess. Every pocket stuffed with
pennies and half-pennies--421 pennies and 270 half-pennies. It
was no wonder that it had not been swept away by the tide. But a
human body is a different matter. There is a fierce eddy between
the wharf and the house. It seemed likely enough that the
weighted coat had remained when the stripped body had been sucked
away into the river."
"But I understand that all the other clothes were found in the
room. Would the body be dressed in a coat alone?"
"No, sir, but the facts might be met speciously enough. Suppose
that this man Boone had thrust Neville St. Clair through the
window, there is no human eye which could have seen the deed.
What would he do then? It would of course instantly strike him
that he must get rid of the tell-tale garments. He would seize
the coat, then, and be in the act of throwing it out, when it
would occur to him that it would swim and not sink. He has little
time, for he has heard the scuffle downstairs when the wife tried
to force her way up, and perhaps he has already heard from his
Lascar confederate that the police are hurrying up the street.
There is not an instant to be lost. He rushes to some secret
hoard, where he has accumulated the fruits of his beggary, and he
stuffs all the coins upon which he can lay his hands into the
pockets to make sure of the coat's sinking. He throws it out, and
would have done the same with the other garments had not he heard
the rush of steps below, and only just had time to close the
window when the police appeared."
"It certainly sounds feasible."
"Well, we will take it as a working hypothesis for want of a
better. Boone, as I have told you, was arrested and taken to the
station, but it could not be shown that there had ever before
been anything against him. He had for years been known as a
professional beggar, but his life appeared to have been a very
quiet and innocent one. There the matter stands at present, and
the questions which have to be solved--what Neville St. Clair was
doing in the opium den, what happened to him when there, where is
he now, and what Hugh Boone had to do with his disappearance--are
all as far from a solution as ever. I confess that I cannot
recall any case within my experience which looked at the first
glance so simple and yet which presented such difficulties."
While Sherlock Holmes had been detailing this singular series of
events, we had been whirling through the outskirts of the great
town until the last straggling houses had been left behind, and
we rattled along with a country hedge upon either side of us.
Just as he finished, however, we drove through two scattered
villages, where a few lights still glimmered in the windows.
"We are on the outskirts of Lee," said my companion. "We have
touched on three English counties in our short drive, starting in
Middlesex, passing over an angle of Surrey, and ending in Kent.
See that light among the trees? That is The Cedars, and beside
that lamp sits a woman whose anxious ears have already, I have
little doubt, caught the clink of our horse's feet."
"But why are you not conducting the case from Baker Street?" I
asked.
"Because there are many inquiries which must be made out here.
Mrs. St. Clair has most kindly put two rooms at my disposal, and
you may rest assured that she will have nothing but a welcome for
my friend and colleague. I hate to meet her, Watson, when I have
no news of her husband. Here we are. Whoa, there, whoa!"
We had pulled up in front of a large villa which stood within its
own grounds. A stable-boy had run out to the horse's head, and
springing down, I followed Holmes up the small, winding
gravel-drive which led to the house. As we approached, the door
flew open, and a little blonde woman stood in the opening, clad
in some sort of light mousseline de soie, with a touch of fluffy
pink chiffon at her neck and wrists. She stood with her figure
outlined against the flood of light, one hand upon the door, one
half-raised in her eagerness, her body slightly bent, her head
and face protruded, with eager eyes and parted lips, a standing
question.
"Well?" she cried, "well?" And then, seeing that there were two
of us, she gave a cry of hope which sank into a groan as she saw
that my companion shook his head and shrugged his shoulders.
"No good news?"
"None."
"No bad?"
"No."
"Thank God for that. But come in. You must be weary, for you have
had a long day."
"This is my friend, Dr. Watson. He has been of most vital use to
me in several of my cases, and a lucky chance has made it
possible for me to bring him out and associate him with this
investigation."
"I am delighted to see you," said she, pressing my hand warmly.
"You will, I am sure, forgive anything that may be wanting in our
arrangements, when you consider the blow which has come so
suddenly upon us."
"My dear madam," said I, "I am an old campaigner, and if I were
not I can very well see that no apology is needed. If I can be of
any assistance, either to you or to my friend here, I shall be
indeed happy."
"Now, Mr. Sherlock Holmes," said the lady as we entered a
well-lit dining-room, upon the table of which a cold supper had
been laid out, "I should very much like to ask you one or two
plain questions, to which I beg that you will give a plain
answer."
"Certainly, madam."
"Do not trouble about my feelings. I am not hysterical, nor given
to fainting. I simply wish to hear your real, real opinion."
"Upon what point?"
"In your heart of hearts, do you think that Neville is alive?"
Sherlock Holmes seemed to be embarrassed by the question.
"Frankly, now!" she repeated, standing upon the rug and looking
keenly down at him as he leaned back in a basket-chair.
"Frankly, then, madam, I do not."
"You think that he is dead?"
"I do."
"Murdered?"
"I don't say that. Perhaps."
"And on what day did he meet his death?"
"On Monday."
"Then perhaps, Mr. Holmes, you will be good enough to explain how
it is that I have received a letter from him to-day."
Sherlock Holmes sprang out of his chair as if he had been
galvanised.
"What!" he roared.
"Yes, to-day." She stood smiling, holding up a little slip of
paper in the air.
"May I see it?"
"Certainly."
He snatched it from her in his eagerness, and smoothing it out
upon the table he drew over the lamp and examined it intently. I
had left my chair and was gazing at it over his shoulder. The
envelope was a very coarse one and was stamped with the Gravesend
postmark and with the date of that very day, or rather of the day
before, for it was considerably after midnight.
"Coarse writing," murmured Holmes. "Surely this is not your
husband's writing, madam."
"No, but the enclosure is."
"I perceive also that whoever addressed the envelope had to go
and inquire as to the address."
"How can you tell that?"
"The name, you see, is in perfectly black ink, which has dried
itself. The rest is of the greyish colour, which shows that
blotting-paper has been used. If it had been written straight
off, and then blotted, none would be of a deep black shade. This
man has written the name, and there has then been a pause before
he wrote the address, which can only mean that he was not
familiar with it. It is, of course, a trifle, but there is
nothing so important as trifles. Let us now see the letter. Ha!
there has been an enclosure here!"
"Yes, there was a ring. His signet-ring."
"And you are sure that this is your husband's hand?"
"One of his hands."
"One?"
"His hand when he wrote hurriedly. It is very unlike his usual
writing, and yet I know it well."
"'Dearest do not be frightened. All will come well. There is a
huge error which it may take some little time to rectify.
Wait in patience.--NEVILLE.' Written in pencil upon the fly-leaf
of a book, octavo size, no water-mark. Hum! Posted to-day in
Gravesend by a man with a dirty thumb. Ha! And the flap has been
gummed, if I am not very much in error, by a person who had been
chewing tobacco. And you have no doubt that it is your husband's
hand, madam?"
"None. Neville wrote those words."
"And they were posted to-day at Gravesend. Well, Mrs. St. Clair,
the clouds lighten, though I should not venture to say that the
danger is over."
"But he must be alive, Mr. Holmes."
"Unless this is a clever forgery to put us on the wrong scent.
The ring, after all, proves nothing. It may have been taken from
him."
"No, no; it is, it is his very own writing!"
"Very well. It may, however, have been written on Monday and only
posted to-day."
"That is possible."
"If so, much may have happened between."
"Oh, you must not discourage me, Mr. Holmes. I know that all is
well with him. There is so keen a sympathy between us that I
should know if evil came upon him. On the very day that I saw him
last he cut himself in the bedroom, and yet I in the dining-room
rushed upstairs instantly with the utmost certainty that
something had happened. Do you think that I would respond to such
a trifle and yet be ignorant of his death?"
"I have seen too much not to know that the impression of a woman
may be more valuable than the conclusion of an analytical
reasoner. And in this letter you certainly have a very strong
piece of evidence to corroborate your view. But if your husband
is alive and able to write letters, why should he remain away
from you?"
"I cannot imagine. It is unthinkable."
"And on Monday he made no remarks before leaving you?"
"No."
"And you were surprised to see him in Swandam Lane?"
"Very much so."
"Was the window open?"
"Yes."
"Then he might have called to you?"
"He might."
"He only, as I understand, gave an inarticulate cry?"
"Yes."
"A call for help, you thought?"
"Yes. He waved his hands."
"But it might have been a cry of surprise. Astonishment at the
unexpected sight of you might cause him to throw up his hands?"
"It is possible."
"And you thought he was pulled back?"
"He disappeared so suddenly."
"He might have leaped back. You did not see anyone else in the
room?"
"No, but this horrible man confessed to having been there, and
the Lascar was at the foot of the stairs."
"Quite so. Your husband, as far as you could see, had his
ordinary clothes on?"
"But without his collar or tie. I distinctly saw his bare
throat."
"Had he ever spoken of Swandam Lane?"
"Never."
"Had he ever showed any signs of having taken opium?"
"Never."
"Thank you, Mrs. St. Clair. Those are the principal points about
which I wished to be absolutely clear. We shall now have a little
supper and then retire, for we may have a very busy day
to-morrow."
A large and comfortable double-bedded room had been placed at our
disposal, and I was quickly between the sheets, for I was weary
after my night of adventure. Sherlock Holmes was a man, however,
who, when he had an unsolved problem upon his mind, would go for
days, and even for a week, without rest, turning it over,
rearranging his facts, looking at it from every point of view
until he had either fathomed it or convinced himself that his
data were insufficient. It was soon evident to me that he was now
preparing for an all-night sitting. He took off his coat and
waistcoat, put on a large blue dressing-gown, and then wandered
about the room collecting pillows from his bed and cushions from
the sofa and armchairs. With these he constructed a sort of
Eastern divan, upon which he perched himself cross-legged, with
an ounce of shag tobacco and a box of matches laid out in front
of him. In the dim light of the lamp I saw him sitting there, an
old briar pipe between his lips, his eyes fixed vacantly upon the
corner of the ceiling, the blue smoke curling up from him,
silent, motionless, with the light shining upon his strong-set
aquiline features. So he sat as I dropped off to sleep, and so he
sat when a sudden ejaculation caused me to wake up, and I found
the summer sun shining into the apartment. The pipe was still
between his lips, the smoke still curled upward, and the room was
full of a dense tobacco haze, but nothing remained of the heap of
shag which I had seen upon the previous night.
"Awake, Watson?" he asked.
"Yes."
"Game for a morning drive?"
"Certainly."
"Then dress. No one is stirring yet, but I know where the
stable-boy sleeps, and we shall soon have the trap out." He
chuckled to himself as he spoke, his eyes twinkled, and he seemed
a different man to the sombre thinker of the previous night.
As I dressed I glanced at my watch. It was no wonder that no one
was stirring. It was twenty-five minutes past four. I had hardly
finished when Holmes returned with the news that the boy was
putting in the horse.
"I want to test a little theory of mine," said he, pulling on his
boots. "I think, Watson, that you are now standing in the
presence of one of the most absolute fools in Europe. I deserve
to be kicked from here to Charing Cross. But I think I have the
key of the affair now."
"And where is it?" I asked, smiling.
"In the bathroom," he answered. "Oh, yes, I am not joking," he
continued, seeing my look of incredulity. "I have just been
there, and I have taken it out, and I have got it in this
Gladstone bag. Come on, my boy, and we shall see whether it will
not fit the lock."
We made our way downstairs as quietly as possible, and out into
the bright morning sunshine. In the road stood our horse and
trap, with the half-clad stable-boy waiting at the head. We both
sprang in, and away we dashed down the London Road. A few country
carts were stirring, bearing in vegetables to the metropolis, but
the lines of villas on either side were as silent and lifeless as
some city in a dream.
"It has been in some points a singular case," said Holmes,
flicking the horse on into a gallop. "I confess that I have been
as blind as a mole, but it is better to learn wisdom late than
never to learn it at all."
In town the earliest risers were just beginning to look sleepily
from their windows as we drove through the streets of the Surrey
side. Passing down the Waterloo Bridge Road we crossed over the
river, and dashing up Wellington Street wheeled sharply to the
right and found ourselves in Bow Street. Sherlock Holmes was well
known to the force, and the two constables at the door saluted
him. One of them held the horse's head while the other led us in.
"Who is on duty?" asked Holmes.
"Inspector Bradstreet, sir."
"Ah, Bradstreet, how are you?" A tall, stout official had come
down the stone-flagged passage, in a peaked cap and frogged
jacket. "I wish to have a quiet word with you, Bradstreet."
"Certainly, Mr. Holmes. Step into my room here." It was a small,
office-like room, with a huge ledger upon the table, and a
telephone projecting from the wall. The inspector sat down at his
desk.
"What can I do for you, Mr. Holmes?"
"I called about that beggarman, Boone--the one who was charged
with being concerned in the disappearance of Mr. Neville St.
Clair, of Lee."
"Yes. He was brought up and remanded for further inquiries."
"So I heard. You have him here?"
"In the cells."
"Is he quiet?"
"Oh, he gives no trouble. But he is a dirty scoundrel."
"Dirty?"
"Yes, it is all we can do to make him wash his hands, and his
face is as black as a tinker's. Well, when once his case has been
settled, he will have a regular prison bath; and I think, if you
saw him, you would agree with me that he needed it."
"I should like to see him very much."
"Would you? That is easily done. Come this way. You can leave
your bag."
"No, I think that I'll take it."
"Very good. Come this way, if you please." He led us down a
passage, opened a barred door, passed down a winding stair, and
brought us to a whitewashed corridor with a line of doors on each
side.
"The third on the right is his," said the inspector. "Here it
is!" He quietly shot back a panel in the upper part of the door
and glanced through.
"He is asleep," said he. "You can see him very well."
We both put our eyes to the grating. The prisoner lay with his
face towards us, in a very deep sleep, breathing slowly and
heavily. He was a middle-sized man, coarsely clad as became his
calling, with a coloured shirt protruding through the rent in his
tattered coat. He was, as the inspector had said, extremely
dirty, but the grime which covered his face could not conceal its
repulsive ugliness. A broad wheal from an old scar ran right
across it from eye to chin, and by its contraction had turned up
one side of the upper lip, so that three teeth were exposed in a
perpetual snarl. A shock of very bright red hair grew low over
his eyes and forehead.
"He's a beauty, isn't he?" said the inspector.
"He certainly needs a wash," remarked Holmes. "I had an idea that
he might, and I took the liberty of bringing the tools with me."
He opened the Gladstone bag as he spoke, and took out, to my
astonishment, a very large bath-sponge.
"He! he! You are a funny one," chuckled the inspector.
"Now, if you will have the great goodness to open that door very
quietly, we will soon make him cut a much more respectable
figure."
"Well, I don't know why not," said the inspector. "He doesn't
look a credit to the Bow Street cells, does he?" He slipped his
key into the lock, and we all very quietly entered the cell. The
sleeper half turned, and then settled down once more into a deep
slumber. Holmes stooped to the water-jug, moistened his sponge,
and then rubbed it twice vigorously across and down the
prisoner's face.
"Let me introduce you," he shouted, "to Mr. Neville St. Clair, of
Lee, in the county of Kent."
Never in my life have I seen such a sight. The man's face peeled
off under the sponge like the bark from a tree. Gone was the
coarse brown tint! Gone, too, was the horrid scar which had
seamed it across, and the twisted lip which had given the
repulsive sneer to the face! A twitch brought away the tangled
red hair, and there, sitting up in his bed, was a pale,
sad-faced, refined-looking man, black-haired and smooth-skinned,
rubbing his eyes and staring about him with sleepy bewilderment.
Then suddenly realising the exposure, he broke into a scream and
threw himself down with his face to the pillow.
"Great heavens!" cried the inspector, "it is, indeed, the missing
man. I know him from the photograph."
The prisoner turned with the reckless air of a man who abandons
himself to his destiny. "Be it so," said he. "And pray what am I
charged with?"
"With making away with Mr. Neville St.-- Oh, come, you can't be
charged with that unless they make a case of attempted suicide of
it," said the inspector with a grin. "Well, I have been
twenty-seven years in the force, but this really takes the cake."
"If I am Mr. Neville St. Clair, then it is obvious that no crime
has been committed, and that, therefore, I am illegally
detained."
"No crime, but a very great error has been committed," said
Holmes. "You would have done better to have trusted your wife."
"It was not the wife; it was the children," groaned the prisoner.
"God help me, I would not have them ashamed of their father. My
God! What an exposure! What can I do?"
Sherlock Holmes sat down beside him on the couch and patted him
kindly on the shoulder.
"If you leave it to a court of law to clear the matter up," said
he, "of course you can hardly avoid publicity. On the other hand,
if you convince the police authorities that there is no possible
case against you, I do not know that there is any reason that the
details should find their way into the papers. Inspector
Bradstreet would, I am sure, make notes upon anything which you
might tell us and submit it to the proper authorities. The case
would then never go into court at all."
"God bless you!" cried the prisoner passionately. "I would have
endured imprisonment, ay, even execution, rather than have left
my miserable secret as a family blot to my children.
"You are the first who have ever heard my story. My father was a
schoolmaster in Chesterfield, where I received an excellent
education. I travelled in my youth, took to the stage, and
finally became a reporter on an evening paper in London. One day
my editor wished to have a series of articles upon begging in the
metropolis, and I volunteered to supply them. There was the point
from which all my adventures started. It was only by trying
begging as an amateur that I could get the facts upon which to
base my articles. When an actor I had, of course, learned all the
secrets of making up, and had been famous in the green-room for
my skill. I took advantage now of my attainments. I painted my
face, and to make myself as pitiable as possible I made a good
scar and fixed one side of my lip in a twist by the aid of a
small slip of flesh-coloured plaster. Then with a red head of
hair, and an appropriate dress, I took my station in the business
part of the city, ostensibly as a match-seller but really as a
beggar. For seven hours I plied my trade, and when I returned
home in the evening I found to my surprise that I had received no
less than 26s. 4d.
"I wrote my articles and thought little more of the matter until,
some time later, I backed a bill for a friend and had a writ
served upon me for 25 pounds. I was at my wit's end where to get
the money, but a sudden idea came to me. I begged a fortnight's
grace from the creditor, asked for a holiday from my employers,
and spent the time in begging in the City under my disguise. In
ten days I had the money and had paid the debt.
"Well, you can imagine how hard it was to settle down to arduous
work at 2 pounds a week when I knew that I could earn as much in
a day by smearing my face with a little paint, laying my cap on
the ground, and sitting still. It was a long fight between my
pride and the money, but the dollars won at last, and I threw up
reporting and sat day after day in the corner which I had first
chosen, inspiring pity by my ghastly face and filling my pockets
with coppers. Only one man knew my secret. He was the keeper of a
low den in which I used to lodge in Swandam Lane, where I could
every morning emerge as a squalid beggar and in the evenings
transform myself into a well-dressed man about town. This fellow,
a Lascar, was well paid by me for his rooms, so that I knew that
my secret was safe in his possession.
"Well, very soon I found that I was saving considerable sums of
money. I do not mean that any beggar in the streets of London
could earn 700 pounds a year--which is less than my average
takings--but I had exceptional advantages in my power of making
up, and also in a facility of repartee, which improved by
practice and made me quite a recognised character in the City.
All day a stream of pennies, varied by silver, poured in upon me,
and it was a very bad day in which I failed to take 2 pounds.
"As I grew richer I grew more ambitious, took a house in the
country, and eventually married, without anyone having a
suspicion as to my real occupation. My dear wife knew that I had
business in the City. She little knew what.
"Last Monday I had finished for the day and was dressing in my
room above the opium den when I looked out of my window and saw,
to my horror and astonishment, that my wife was standing in the
street, with her eyes fixed full upon me. I gave a cry of
surprise, threw up my arms to cover my face, and, rushing to my
confidant, the Lascar, entreated him to prevent anyone from
coming up to me. I heard her voice downstairs, but I knew that
she could not ascend. Swiftly I threw off my clothes, pulled on
those of a beggar, and put on my pigments and wig. Even a wife's
eyes could not pierce so complete a disguise. But then it
occurred to me that there might be a search in the room, and that
the clothes might betray me. I threw open the window, reopening
by my violence a small cut which I had inflicted upon myself in
the bedroom that morning. Then I seized my coat, which was
weighted by the coppers which I had just transferred to it from
the leather bag in which I carried my takings. I hurled it out of
the window, and it disappeared into the Thames. The other clothes
would have followed, but at that moment there was a rush of
constables up the stair, and a few minutes after I found, rather,
I confess, to my relief, that instead of being identified as Mr.
Neville St. Clair, I was arrested as his murderer.
"I do not know that there is anything else for me to explain. I
was determined to preserve my disguise as long as possible, and
hence my preference for a dirty face. Knowing that my wife would
be terribly anxious, I slipped off my ring and confided it to the
Lascar at a moment when no constable was watching me, together
with a hurried scrawl, telling her that she had no cause to
fear."
"That note only reached her yesterday," said Holmes.
"Good God! What a week she must have spent!"
"The police have watched this Lascar," said Inspector Bradstreet,
"and I can quite understand that he might find it difficult to
post a letter unobserved. Probably he handed it to some sailor
customer of his, who forgot all about it for some days."
"That was it," said Holmes, nodding approvingly; "I have no doubt
of it. But have you never been prosecuted for begging?"
"Many times; but what was a fine to me?"
"It must stop here, however," said Bradstreet. "If the police are
to hush this thing up, there must be no more of Hugh Boone."
"I have sworn it by the most solemn oaths which a man can take."
"In that case I think that it is probable that no further steps
may be taken. But if you are found again, then all must come out.
I am sure, Mr. Holmes, that we are very much indebted to you for
having cleared the matter up. I wish I knew how you reach your
results."
"I reached this one," said my friend, "by sitting upon five
pillows and consuming an ounce of shag. I think, Watson, that if
we drive to Baker Street we shall just be in time for breakfast."
VII. THE ADVENTURE OF THE BLUE CARBUNCLE
I had called upon my friend Sherlock Holmes upon the second
morning after Christmas, with the intention of wishing him the
compliments of the season. He was lounging upon the sofa in a
purple dressing-gown, a pipe-rack within his reach upon the
right, and a pile of crumpled morning papers, evidently newly
studied, near at hand. Beside the couch was a wooden chair, and
on the angle of the back hung a very seedy and disreputable
hard-felt hat, much the worse for wear, and cracked in several
places. A lens and a forceps lying upon the seat of the chair
suggested that the hat had been suspended in this manner for the
purpose of examination.
"You are engaged," said I; "perhaps I interrupt you."
"Not at all. I am glad to have a friend with whom I can discuss
my results. The matter is a perfectly trivial one"--he jerked his
thumb in the direction of the old hat--"but there are points in
connection with it which are not entirely devoid of interest and
even of instruction."
I seated myself in his armchair and warmed my hands before his
crackling fire, for a sharp frost had set in, and the windows
were thick with the ice crystals. "I suppose," I remarked, "that,
homely as it looks, this thing has some deadly story linked on to
it--that it is the clue which will guide you in the solution of
some mystery and the punishment of some crime."
"No, no. No crime," said Sherlock Holmes, laughing. "Only one of
those whimsical little incidents which will happen when you have
four million human beings all jostling each other within the
space of a few square miles. Amid the action and reaction of so
dense a swarm of humanity, every possible combination of events
may be expected to take place, and many a little problem will be
presented which may be striking and bizarre without being
criminal. We have already had experience of such."
"So much so," I remarked, "that of the last six cases which I
have added to my notes, three have been entirely free of any
legal crime."
"Precisely. You allude to my attempt to recover the Irene Adler
papers, to the singular case of Miss Mary Sutherland, and to the
adventure of the man with the twisted lip. Well, I have no doubt
that this small matter will fall into the same innocent category.
You know Peterson, the commissionaire?"
"Yes."
"It is to him that this trophy belongs."
"It is his hat."
"No, no, he found it. Its owner is unknown. I beg that you will
look upon it not as a battered billycock but as an intellectual
problem. And, first, as to how it came here. It arrived upon
Christmas morning, in company with a good fat goose, which is, I
have no doubt, roasting at this moment in front of Peterson's
fire. The facts are these: about four o'clock on Christmas
morning, Peterson, who, as you know, is a very honest fellow, was
returning from some small jollification and was making his way
homeward down Tottenham Court Road. In front of him he saw, in
the gaslight, a tallish man, walking with a slight stagger, and
carrying a white goose slung over his shoulder. As he reached the
corner of Goodge Street, a row broke out between this stranger
and a little knot of roughs. One of the latter knocked off the
man's hat, on which he raised his stick to defend himself and,
swinging it over his head, smashed the shop window behind him.
Peterson had rushed forward to protect the stranger from his
assailants; but the man, shocked at having broken the window, and
seeing an official-looking person in uniform rushing towards him,
dropped his goose, took to his heels, and vanished amid the
labyrinth of small streets which lie at the back of Tottenham
Court Road. The roughs had also fled at the appearance of
Peterson, so that he was left in possession of the field of
battle, and also of the spoils of victory in the shape of this
battered hat and a most unimpeachable Christmas goose."
"Which surely he restored to their owner?"
"My dear fellow, there lies the problem. It is true that 'For
Mrs. Henry Baker' was printed upon a small card which was tied to
the bird's left leg, and it is also true that the initials 'H.
B.' are legible upon the lining of this hat, but as there are
some thousands of Bakers, and some hundreds of Henry Bakers in
this city of ours, it is not easy to restore lost property to any
one of them."
"What, then, did Peterson do?"
"He brought round both hat and goose to me on Christmas morning,
knowing that even the smallest problems are of interest to me.
The goose we retained until this morning, when there were signs
that, in spite of the slight frost, it would be well that it
should be eaten without unnecessary delay. Its finder has carried
it off, therefore, to fulfil the ultimate destiny of a goose,
while I continue to retain the hat of the unknown gentleman who
lost his Christmas dinner."
"Did he not advertise?"
"No."
"Then, what clue could you have as to his identity?"
"Only as much as we can deduce."
"From his hat?"
"Precisely."
"But you are joking. What can you gather from this old battered
felt?"
"Here is my lens. You know my methods. What can you gather
yourself as to the individuality of the man who has worn this
article?"
I took the tattered object in my hands and turned it over rather
ruefully. It was a very ordinary black hat of the usual round
shape, hard and much the worse for wear. The lining had been of
red silk, but was a good deal discoloured. There was no maker's
name; but, as Holmes had remarked, the initials "H. B." were
scrawled upon one side. It was pierced in the brim for a
hat-securer, but the elastic was missing. For the rest, it was
cracked, exceedingly dusty, and spotted in several places,
although there seemed to have been some attempt to hide the
discoloured patches by smearing them with ink.
"I can see nothing," said I, handing it back to my friend.
"On the contrary, Watson, you can see everything. You fail,
however, to reason from what you see. You are too timid in
drawing your inferences."
"Then, pray tell me what it is that you can infer from this hat?"
He picked it up and gazed at it in the peculiar introspective
fashion which was characteristic of him. "It is perhaps less
suggestive than it might have been," he remarked, "and yet there
are a few inferences which are very distinct, and a few others
which represent at least a strong balance of probability. That
the man was highly intellectual is of course obvious upon the
face of it, and also that he was fairly well-to-do within the
last three years, although he has now fallen upon evil days. He
had foresight, but has less now than formerly, pointing to a
moral retrogression, which, when taken with the decline of his
fortunes, seems to indicate some evil influence, probably drink,
at work upon him. This may account also for the obvious fact that
his wife has ceased to love him."
"My dear Holmes!"
"He has, however, retained some degree of self-respect," he
continued, disregarding my remonstrance. "He is a man who leads a
sedentary life, goes out little, is out of training entirely, is
middle-aged, has grizzled hair which he has had cut within the
last few days, and which he anoints with lime-cream. These are
the more patent facts which are to be deduced from his hat. Also,
by the way, that it is extremely improbable that he has gas laid
on in his house."
"You are certainly joking, Holmes."
"Not in the least. Is it possible that even now, when I give you
these results, you are unable to see how they are attained?"
"I have no doubt that I am very stupid, but I must confess that I
am unable to follow you. For example, how did you deduce that
this man was intellectual?"
For answer Holmes clapped the hat upon his head. It came right
over the forehead and settled upon the bridge of his nose. "It is
a question of cubic capacity," said he; "a man with so large a
brain must have something in it."
"The decline of his fortunes, then?"
"This hat is three years old. These flat brims curled at the edge
came in then. It is a hat of the very best quality. Look at the
band of ribbed silk and the excellent lining. If this man could
afford to buy so expensive a hat three years ago, and has had no
hat since, then he has assuredly gone down in the world."
"Well, that is clear enough, certainly. But how about the
foresight and the moral retrogression?"
Sherlock Holmes laughed. "Here is the foresight," said he putting
his finger upon the little disc and loop of the hat-securer.
"They are never sold upon hats. If this man ordered one, it is a
sign of a certain amount of foresight, since he went out of his
way to take this precaution against the wind. But since we see
that he has broken the elastic and has not troubled to replace
it, it is obvious that he has less foresight now than formerly,
which is a distinct proof of a weakening nature. On the other
hand, he has endeavoured to conceal some of these stains upon the
felt by daubing them with ink, which is a sign that he has not
entirely lost his self-respect."
"Your reasoning is certainly plausible."
"The further points, that he is middle-aged, that his hair is
grizzled, that it has been recently cut, and that he uses
lime-cream, are all to be gathered from a close examination of the
lower part of the lining. The lens discloses a large number of
hair-ends, clean cut by the scissors of the barber. They all
appear to be adhesive, and there is a distinct odour of
lime-cream. This dust, you will observe, is not the gritty, grey
dust of the street but the fluffy brown dust of the house,
showing that it has been hung up indoors most of the time, while
the marks of moisture upon the inside are proof positive that the
wearer perspired very freely, and could therefore, hardly be in
the best of training."
"But his wife--you said that she had ceased to love him."
"This hat has not been brushed for weeks. When I see you, my dear
Watson, with a week's accumulation of dust upon your hat, and
when your wife allows you to go out in such a state, I shall fear
that you also have been unfortunate enough to lose your wife's
affection."
"But he might be a bachelor."
"Nay, he was bringing home the goose as a peace-offering to his
wife. Remember the card upon the bird's leg."
"You have an answer to everything. But how on earth do you deduce
that the gas is not laid on in his house?"
"One tallow stain, or even two, might come by chance; but when I
see no less than five, I think that there can be little doubt
that the individual must be brought into frequent contact with
burning tallow--walks upstairs at night probably with his hat in
one hand and a guttering candle in the other. Anyhow, he never
got tallow-stains from a gas-jet. Are you satisfied?"
"Well, it is very ingenious," said I, laughing; "but since, as
you said just now, there has been no crime committed, and no harm
done save the loss of a goose, all this seems to be rather a
waste of energy."
Sherlock Holmes had opened his mouth to reply, when the door flew
open, and Peterson, the commissionaire, rushed into the apartment
with flushed cheeks and the face of a man who is dazed with
astonishment.
"The goose, Mr. Holmes! The goose, sir!" he gasped.
"Eh? What of it, then? Has it returned to life and flapped off
through the kitchen window?" Holmes twisted himself round upon
the sofa to get a fairer view of the man's excited face.
"See here, sir! See what my wife found in its crop!" He held out
his hand and displayed upon the centre of the palm a brilliantly
scintillating blue stone, rather smaller than a bean in size, but
of such purity and radiance that it twinkled like an electric
point in the dark hollow of his hand.
Sherlock Holmes sat up with a whistle. "By Jove, Peterson!" said
he, "this is treasure trove indeed. I suppose you know what you
have got?"
"A diamond, sir? A precious stone. It cuts into glass as though
it were putty."
"It's more than a precious stone. It is the precious stone."
"Not the Countess of Morcar's blue carbuncle!" I ejaculated.
"Precisely so. I ought to know its size and shape, seeing that I
have read the advertisement about it in The Times every day
lately. It is absolutely unique, and its value can only be
conjectured, but the reward offered of 1000 pounds is certainly
not within a twentieth part of the market price."
"A thousand pounds! Great Lord of mercy!" The commissionaire
plumped down into a chair and stared from one to the other of us.
"That is the reward, and I have reason to know that there are
sentimental considerations in the background which would induce
the Countess to part with half her fortune if she could but
recover the gem."
"It was lost, if I remember aright, at the Hotel Cosmopolitan," I
remarked.
"Precisely so, on December 22nd, just five days ago. John Horner,
a plumber, was accused of having abstracted it from the lady's
jewel-case. The evidence against him was so strong that the case
has been referred to the Assizes. I have some account of the
matter here, I believe." He rummaged amid his newspapers,
glancing over the dates, until at last he smoothed one out,
doubled it over, and read the following paragraph:
"Hotel Cosmopolitan Jewel Robbery. John Horner, 26, plumber, was
brought up upon the charge of having upon the 22nd inst.,
abstracted from the jewel-case of the Countess of Morcar the
valuable gem known as the blue carbuncle. James Ryder,
upper-attendant at the hotel, gave his evidence to the effect
that he had shown Horner up to the dressing-room of the Countess
of Morcar upon the day of the robbery in order that he might
solder the second bar of the grate, which was loose. He had
remained with Horner some little time, but had finally been
called away. On returning, he found that Horner had disappeared,
that the bureau had been forced open, and that the small morocco
casket in which, as it afterwards transpired, the Countess was
accustomed to keep her jewel, was lying empty upon the
dressing-table. Ryder instantly gave the alarm, and Horner was
arrested the same evening; but the stone could not be found
either upon his person or in his rooms. Catherine Cusack, maid to
the Countess, deposed to having heard Ryder's cry of dismay on
discovering the robbery, and to having rushed into the room,
where she found matters as described by the last witness.
Inspector Bradstreet, B division, gave evidence as to the arrest
of Horner, who struggled frantically, and protested his innocence
in the strongest terms. Evidence of a previous conviction for
robbery having been given against the prisoner, the magistrate
refused to deal summarily with the offence, but referred it to
the Assizes. Horner, who had shown signs of intense emotion
during the proceedings, fainted away at the conclusion and was
carried out of court."
"Hum! So much for the police-court," said Holmes thoughtfully,
tossing aside the paper. "The question for us now to solve is the
sequence of events leading from a rifled jewel-case at one end to
the crop of a goose in Tottenham Court Road at the other. You
see, Watson, our little deductions have suddenly assumed a much
more important and less innocent aspect. Here is the stone; the
stone came from the goose, and the goose came from Mr. Henry
Baker, the gentleman with the bad hat and all the other
characteristics with which I have bored you. So now we must set
ourselves very seriously to finding this gentleman and
ascertaining what part he has played in this little mystery. To
do this, we must try the simplest means first, and these lie
undoubtedly in an advertisement in all the evening papers. If
this fail, I shall have recourse to other methods."
"What will you say?"
"Give me a pencil and that slip of paper. Now, then: 'Found at
the corner of Goodge Street, a goose and a black felt hat. Mr.
Henry Baker can have the same by applying at 6:30 this evening at
221B, Baker Street.' That is clear and concise."
"Very. But will he see it?"
"Well, he is sure to keep an eye on the papers, since, to a poor
man, the loss was a heavy one. He was clearly so scared by his
mischance in breaking the window and by the approach of Peterson
that he thought of nothing but flight, but since then he must
have bitterly regretted the impulse which caused him to drop his
bird. Then, again, the introduction of his name will cause him to
see it, for everyone who knows him will direct his attention to
it. Here you are, Peterson, run down to the advertising agency
and have this put in the evening papers."
"In which, sir?"
"Oh, in the Globe, Star, Pall Mall, St. James's, Evening News,
Standard, Echo, and any others that occur to you."
"Very well, sir. And this stone?"
"Ah, yes, I shall keep the stone. Thank you. And, I say,
Peterson, just buy a goose on your way back and leave it here
with me, for we must have one to give to this gentleman in place
of the one which your family is now devouring."
When the commissionaire had gone, Holmes took up the stone and
held it against the light. "It's a bonny thing," said he. "Just
see how it glints and sparkles. Of course it is a nucleus and
focus of crime. Every good stone is. They are the devil's pet
baits. In the larger and older jewels every facet may stand for a
bloody deed. This stone is not yet twenty years old. It was found
in the banks of the Amoy River in southern China and is remarkable
in having every characteristic of the carbuncle, save that it is
blue in shade instead of ruby red. In spite of its youth, it has
already a sinister history. There have been two murders, a
vitriol-throwing, a suicide, and several robberies brought about
for the sake of this forty-grain weight of crystallised charcoal.
Who would think that so pretty a toy would be a purveyor to the
gallows and the prison? I'll lock it up in my strong box now and
drop a line to the Countess to say that we have it."
"Do you think that this man Horner is innocent?"
"I cannot tell."
"Well, then, do you imagine that this other one, Henry Baker, had
anything to do with the matter?"
"It is, I think, much more likely that Henry Baker is an
absolutely innocent man, who had no idea that the bird which he
was carrying was of considerably more value than if it were made
of solid gold. That, however, I shall determine by a very simple
test if we have an answer to our advertisement."
"And you can do nothing until then?"
"Nothing."
"In that case I shall continue my professional round. But I shall
come back in the evening at the hour you have mentioned, for I
should like to see the solution of so tangled a business."
"Very glad to see you. I dine at seven. There is a woodcock, I
believe. By the way, in view of recent occurrences, perhaps I
ought to ask Mrs. Hudson to examine its crop."
I had been delayed at a case, and it was a little after half-past
six when I found myself in Baker Street once more. As I
approached the house I saw a tall man in a Scotch bonnet with a
coat which was buttoned up to his chin waiting outside in the
bright semicircle which was thrown from the fanlight. Just as I
arrived the door was opened, and we were shown up together to
Holmes' room.
"Mr. Henry Baker, I believe," said he, rising from his armchair
and greeting his visitor with the easy air of geniality which he
could so readily assume. "Pray take this chair by the fire, Mr.
Baker. It is a cold night, and I observe that your circulation is
more adapted for summer than for winter. Ah, Watson, you have
just come at the right time. Is that your hat, Mr. Baker?"
"Yes, sir, that is undoubtedly my hat."
He was a large man with rounded shoulders, a massive head, and a
broad, intelligent face, sloping down to a pointed beard of
grizzled brown. A touch of red in nose and cheeks, with a slight
tremor of his extended hand, recalled Holmes' surmise as to his
habits. His rusty black frock-coat was buttoned right up in
front, with the collar turned up, and his lank wrists protruded
from his sleeves without a sign of cuff or shirt. He spoke in a
slow staccato fashion, choosing his words with care, and gave the
impression generally of a man of learning and letters who had had
ill-usage at the hands of fortune.
"We have retained these things for some days," said Holmes,
"because we expected to see an advertisement from you giving your
address. I am at a loss to know now why you did not advertise."
Our visitor gave a rather shamefaced laugh. "Shillings have not
been so plentiful with me as they once were," he remarked. "I had
no doubt that the gang of roughs who assaulted me had carried off
both my hat and the bird. I did not care to spend more money in a
hopeless attempt at recovering them."
"Very naturally. By the way, about the bird, we were compelled to
eat it."
"To eat it!" Our visitor half rose from his chair in his
excitement.
"Yes, it would have been of no use to anyone had we not done so.
But I presume that this other goose upon the sideboard, which is
about the same weight and perfectly fresh, will answer your
purpose equally well?"
"Oh, certainly, certainly," answered Mr. Baker with a sigh of
relief.
"Of course, we still have the feathers, legs, crop, and so on of
your own bird, so if you wish--"
The man burst into a hearty laugh. "They might be useful to me as
relics of my adventure," said he, "but beyond that I can hardly
see what use the disjecta membra of my late acquaintance are
going to be to me. No, sir, I think that, with your permission, I
will confine my attentions to the excellent bird which I perceive
upon the sideboard."
Sherlock Holmes glanced sharply across at me with a slight shrug
of his shoulders.
"There is your hat, then, and there your bird," said he. "By the
way, would it bore you to tell me where you got the other one
from? I am somewhat of a fowl fancier, and I have seldom seen a
better grown goose."
"Certainly, sir," said Baker, who had risen and tucked his newly
gained property under his arm. "There are a few of us who
frequent the Alpha Inn, near the Museum--we are to be found in
the Museum itself during the day, you understand. This year our
good host, Windigate by name, instituted a goose club, by which,
on consideration of some few pence every week, we were each to
receive a bird at Christmas. My pence were duly paid, and the
rest is familiar to you. I am much indebted to you, sir, for a
Scotch bonnet is fitted neither to my years nor my gravity." With
a comical pomposity of manner he bowed solemnly to both of us and
strode off upon his way.
"So much for Mr. Henry Baker," said Holmes when he had closed the
door behind him. "It is quite certain that he knows nothing
whatever about the matter. Are you hungry, Watson?"
"Not particularly."
"Then I suggest that we turn our dinner into a supper and follow
up this clue while it is still hot."
"By all means."
It was a bitter night, so we drew on our ulsters and wrapped
cravats about our throats. Outside, the stars were shining coldly
in a cloudless sky, and the breath of the passers-by blew out
into smoke like so many pistol shots. Our footfalls rang out
crisply and loudly as we swung through the doctors' quarter,
Wimpole Street, Harley Street, and so through Wigmore Street into
Oxford Street. In a quarter of an hour we were in Bloomsbury at
the Alpha Inn, which is a small public-house at the corner of one
of the streets which runs down into Holborn. Holmes pushed open
the door of the private bar and ordered two glasses of beer from
the ruddy-faced, white-aproned landlord.
"Your beer should be excellent if it is as good as your geese,"
said he.
"My geese!" The man seemed surprised.
"Yes. I was speaking only half an hour ago to Mr. Henry Baker,
who was a member of your goose club."
"Ah! yes, I see. But you see, sir, them's not our geese."
"Indeed! Whose, then?"
"Well, I got the two dozen from a salesman in Covent Garden."
"Indeed? I know some of them. Which was it?"
"Breckinridge is his name."
"Ah! I don't know him. Well, here's your good health landlord,
and prosperity to your house. Good-night."
"Now for Mr. Breckinridge," he continued, buttoning up his coat
as we came out into the frosty air. "Remember, Watson that though
we have so homely a thing as a goose at one end of this chain, we
have at the other a man who will certainly get seven years' penal
servitude unless we can establish his innocence. It is possible
that our inquiry may but confirm his guilt; but, in any case, we
have a line of investigation which has been missed by the police,
and which a singular chance has placed in our hands. Let us
follow it out to the bitter end. Faces to the south, then, and
quick march!"
We passed across Holborn, down Endell Street, and so through a
zigzag of slums to Covent Garden Market. One of the largest
stalls bore the name of Breckinridge upon it, and the proprietor
a horsey-looking man, with a sharp face and trim side-whiskers was
helping a boy to put up the shutters.
"Good-evening. It's a cold night," said Holmes.
The salesman nodded and shot a questioning glance at my
companion.
"Sold out of geese, I see," continued Holmes, pointing at the
bare slabs of marble.
"Let you have five hundred to-morrow morning."
"That's no good."
"Well, there are some on the stall with the gas-flare."
"Ah, but I was recommended to you."
"Who by?"
"The landlord of the Alpha."
"Oh, yes; I sent him a couple of dozen."
"Fine birds they were, too. Now where did you get them from?"
To my surprise the question provoked a burst of anger from the
salesman.
"Now, then, mister," said he, with his head cocked and his arms
akimbo, "what are you driving at? Let's have it straight, now."
"It is straight enough. I should like to know who sold you the
geese which you supplied to the Alpha."
"Well then, I shan't tell you. So now!"
"Oh, it is a matter of no importance; but I don't know why you
should be so warm over such a trifle."
"Warm! You'd be as warm, maybe, if you were as pestered as I am.
When I pay good money for a good article there should be an end
of the business; but it's 'Where are the geese?' and 'Who did you
sell the geese to?' and 'What will you take for the geese?' One
would think they were the only geese in the world, to hear the
fuss that is made over them."
"Well, I have no connection with any other people who have been
making inquiries," said Holmes carelessly. "If you won't tell us
the bet is off, that is all. But I'm always ready to back my
opinion on a matter of fowls, and I have a fiver on it that the
bird I ate is country bred."
"Well, then, you've lost your fiver, for it's town bred," snapped
the salesman.
"It's nothing of the kind."
"I say it is."
"I don't believe it."
"D'you think you know more about fowls than I, who have handled
them ever since I was a nipper? I tell you, all those birds that
went to the Alpha were town bred."
"You'll never persuade me to believe that."
"Will you bet, then?"
"It's merely taking your money, for I know that I am right. But
I'll have a sovereign on with you, just to teach you not to be
obstinate."
The salesman chuckled grimly. "Bring me the books, Bill," said
he.
The small boy brought round a small thin volume and a great
greasy-backed one, laying them out together beneath the hanging
lamp.
"Now then, Mr. Cocksure," said the salesman, "I thought that I
was out of geese, but before I finish you'll find that there is
still one left in my shop. You see this little book?"
"Well?"
"That's the list of the folk from whom I buy. D'you see? Well,
then, here on this page are the country folk, and the numbers
after their names are where their accounts are in the big ledger.
Now, then! You see this other page in red ink? Well, that is a
list of my town suppliers. Now, look at that third name. Just
read it out to me."
"Mrs. Oakshott, 117, Brixton Road--249," read Holmes.
"Quite so. Now turn that up in the ledger."
Holmes turned to the page indicated. "Here you are, 'Mrs.
Oakshott, 117, Brixton Road, egg and poultry supplier.'"
"Now, then, what's the last entry?"
"'December 22nd. Twenty-four geese at 7s. 6d.'"
"Quite so. There you are. And underneath?"
"'Sold to Mr. Windigate of the Alpha, at 12s.'"
"What have you to say now?"
Sherlock Holmes looked deeply chagrined. He drew a sovereign from
his pocket and threw it down upon the slab, turning away with the
air of a man whose disgust is too deep for words. A few yards off
he stopped under a lamp-post and laughed in the hearty, noiseless
fashion which was peculiar to him.
"When you see a man with whiskers of that cut and the 'Pink 'un'
protruding out of his pocket, you can always draw him by a bet,"
said he. "I daresay that if I had put 100 pounds down in front of
him, that man would not have given me such complete information
as was drawn from him by the idea that he was doing me on a
wager. Well, Watson, we are, I fancy, nearing the end of our
quest, and the only point which remains to be determined is
whether we should go on to this Mrs. Oakshott to-night, or
whether we should reserve it for to-morrow. It is clear from what
that surly fellow said that there are others besides ourselves
who are anxious about the matter, and I should--"
His remarks were suddenly cut short by a loud hubbub which broke
out from the stall which we had just left. Turning round we saw a
little rat-faced fellow standing in the centre of the circle of
yellow light which was thrown by the swinging lamp, while
Breckinridge, the salesman, framed in the door of his stall, was
shaking his fists fiercely at the cringing figure.
"I've had enough of you and your geese," he shouted. "I wish you
were all at the devil together. If you come pestering me any more
with your silly talk I'll set the dog at you. You bring Mrs.
Oakshott here and I'll answer her, but what have you to do with
it? Did I buy the geese off you?"
"No; but one of them was mine all the same," whined the little
man.
"Well, then, ask Mrs. Oakshott for it."
"She told me to ask you."
"Well, you can ask the King of Proosia, for all I care. I've had
enough of it. Get out of this!" He rushed fiercely forward, and
the inquirer flitted away into the darkness.
"Ha! this may save us a visit to Brixton Road," whispered Holmes.
"Come with me, and we will see what is to be made of this
fellow." Striding through the scattered knots of people who
lounged round the flaring stalls, my companion speedily overtook
the little man and touched him upon the shoulder. He sprang
round, and I could see in the gas-light that every vestige of
colour had been driven from his face.
"Who are you, then? What do you want?" he asked in a quavering
voice.
"You will excuse me," said Holmes blandly, "but I could not help
overhearing the questions which you put to the salesman just now.
I think that I could be of assistance to you."
"You? Who are you? How could you know anything of the matter?"
"My name is Sherlock Holmes. It is my business to know what other
people don't know."
"But you can know nothing of this?"
"Excuse me, I know everything of it. You are endeavouring to
trace some geese which were sold by Mrs. Oakshott, of Brixton
Road, to a salesman named Breckinridge, by him in turn to Mr.
Windigate, of the Alpha, and by him to his club, of which Mr.
Henry Baker is a member."
"Oh, sir, you are the very man whom I have longed to meet," cried
the little fellow with outstretched hands and quivering fingers.
"I can hardly explain to you how interested I am in this matter."
Sherlock Holmes hailed a four-wheeler which was passing. "In that
case we had better discuss it in a cosy room rather than in this
wind-swept market-place," said he. "But pray tell me, before we
go farther, who it is that I have the pleasure of assisting."
The man hesitated for an instant. "My name is John Robinson," he
answered with a sidelong glance.
"No, no; the real name," said Holmes sweetly. "It is always
awkward doing business with an alias."
A flush sprang to the white cheeks of the stranger. "Well then,"
said he, "my real name is James Ryder."
"Precisely so. Head attendant at the Hotel Cosmopolitan. Pray
step into the cab, and I shall soon be able to tell you
everything which you would wish to know."
The little man stood glancing from one to the other of us with
half-frightened, half-hopeful eyes, as one who is not sure
whether he is on the verge of a windfall or of a catastrophe.
Then he stepped into the cab, and in half an hour we were back in
the sitting-room at Baker Street. Nothing had been said during
our drive, but the high, thin breathing of our new companion, and
the claspings and unclaspings of his hands, spoke of the nervous
tension within him.
"Here we are!" said Holmes cheerily as we filed into the room.
"The fire looks very seasonable in this weather. You look cold,
Mr. Ryder. Pray take the basket-chair. I will just put on my
slippers before we settle this little matter of yours. Now, then!
You want to know what became of those geese?"
"Yes, sir."
"Or rather, I fancy, of that goose. It was one bird, I imagine in
which you were interested--white, with a black bar across the
tail."
Ryder quivered with emotion. "Oh, sir," he cried, "can you tell
me where it went to?"
"It came here."
"Here?"
"Yes, and a most remarkable bird it proved. I don't wonder that
you should take an interest in it. It laid an egg after it was
dead--the bonniest, brightest little blue egg that ever was seen.
I have it here in my museum."
Our visitor staggered to his feet and clutched the mantelpiece
with his right hand. Holmes unlocked his strong-box and held up
the blue carbuncle, which shone out like a star, with a cold,
brilliant, many-pointed radiance. Ryder stood glaring with a
drawn face, uncertain whether to claim or to disown it.
"The game's up, Ryder," said Holmes quietly. "Hold up, man, or
you'll be into the fire! Give him an arm back into his chair,
Watson. He's not got blood enough to go in for felony with
impunity. Give him a dash of brandy. So! Now he looks a little
more human. What a shrimp it is, to be sure!"
For a moment he had staggered and nearly fallen, but the brandy
brought a tinge of colour into his cheeks, and he sat staring
with frightened eyes at his accuser.
"I have almost every link in my hands, and all the proofs which I
could possibly need, so there is little which you need tell me.
Still, that little may as well be cleared up to make the case
complete. You had heard, Ryder, of this blue stone of the
Countess of Morcar's?"
"It was Catherine Cusack who told me of it," said he in a
crackling voice.
"I see--her ladyship's waiting-maid. Well, the temptation of
sudden wealth so easily acquired was too much for you, as it has
been for better men before you; but you were not very scrupulous
in the means you used. It seems to me, Ryder, that there is the
making of a very pretty villain in you. You knew that this man
Horner, the plumber, had been concerned in some such matter
before, and that suspicion would rest the more readily upon him.
What did you do, then? You made some small job in my lady's
room--you and your confederate Cusack--and you managed that he
should be the man sent for. Then, when he had left, you rifled
the jewel-case, raised the alarm, and had this unfortunate man
arrested. You then--"
Ryder threw himself down suddenly upon the rug and clutched at my
companion's knees. "For God's sake, have mercy!" he shrieked.
"Think of my father! Of my mother! It would break their hearts. I
never went wrong before! I never will again. I swear it. I'll
swear it on a Bible. Oh, don't bring it into court! For Christ's
sake, don't!"
"Get back into your chair!" said Holmes sternly. "It is very well
to cringe and crawl now, but you thought little enough of this
poor Horner in the dock for a crime of which he knew nothing."
"I will fly, Mr. Holmes. I will leave the country, sir. Then the
charge against him will break down."
"Hum! We will talk about that. And now let us hear a true account
of the next act. How came the stone into the goose, and how came
the goose into the open market? Tell us the truth, for there lies
your only hope of safety."
Ryder passed his tongue over his parched lips. "I will tell you
it just as it happened, sir," said he. "When Horner had been
arrested, it seemed to me that it would be best for me to get
away with the stone at once, for I did not know at what moment
the police might not take it into their heads to search me and my
room. There was no place about the hotel where it would be safe.
I went out, as if on some commission, and I made for my sister's
house. She had married a man named Oakshott, and lived in Brixton
Road, where she fattened fowls for the market. All the way there
every man I met seemed to me to be a policeman or a detective;
and, for all that it was a cold night, the sweat was pouring down
my face before I came to the Brixton Road. My sister asked me
what was the matter, and why I was so pale; but I told her that I
had been upset by the jewel robbery at the hotel. Then I went
into the back yard and smoked a pipe and wondered what it would
be best to do.
"I had a friend once called Maudsley, who went to the bad, and
has just been serving his time in Pentonville. One day he had met
me, and fell into talk about the ways of thieves, and how they
could get rid of what they stole. I knew that he would be true to
me, for I knew one or two things about him; so I made up my mind
to go right on to Kilburn, where he lived, and take him into my
confidence. He would show me how to turn the stone into money.
But how to get to him in safety? I thought of the agonies I had
gone through in coming from the hotel. I might at any moment be
seized and searched, and there would be the stone in my waistcoat
pocket. I was leaning against the wall at the time and looking at
the geese which were waddling about round my feet, and suddenly
an idea came into my head which showed me how I could beat the
best detective that ever lived.
"My sister had told me some weeks before that I might have the
pick of her geese for a Christmas present, and I knew that she
was always as good as her word. I would take my goose now, and in
it I would carry my stone to Kilburn. There was a little shed in
the yard, and behind this I drove one of the birds--a fine big
one, white, with a barred tail. I caught it, and prying its bill
open, I thrust the stone down its throat as far as my finger
could reach. The bird gave a gulp, and I felt the stone pass
along its gullet and down into its crop. But the creature flapped
and struggled, and out came my sister to know what was the
matter. As I turned to speak to her the brute broke loose and
fluttered off among the others.
"'Whatever were you doing with that bird, Jem?' says she.
"'Well,' said I, 'you said you'd give me one for Christmas, and I
was feeling which was the fattest.'
"'Oh,' says she, 'we've set yours aside for you--Jem's bird, we
call it. It's the big white one over yonder. There's twenty-six
of them, which makes one for you, and one for us, and two dozen
for the market.'
"'Thank you, Maggie,' says I; 'but if it is all the same to you,
I'd rather have that one I was handling just now.'
"'The other is a good three pound heavier,' said she, 'and we
fattened it expressly for you.'
"'Never mind. I'll have the other, and I'll take it now,' said I.
"'Oh, just as you like,' said she, a little huffed. 'Which is it
you want, then?'
"'That white one with the barred tail, right in the middle of the
flock.'
"'Oh, very well. Kill it and take it with you.'
"Well, I did what she said, Mr. Holmes, and I carried the bird
all the way to Kilburn. I told my pal what I had done, for he was
a man that it was easy to tell a thing like that to. He laughed
until he choked, and we got a knife and opened the goose. My
heart turned to water, for there was no sign of the stone, and I
knew that some terrible mistake had occurred. I left the bird,
rushed back to my sister's, and hurried into the back yard. There
was not a bird to be seen there.
"'Where are they all, Maggie?' I cried.
"'Gone to the dealer's, Jem.'
"'Which dealer's?'
"'Breckinridge, of Covent Garden.'
"'But was there another with a barred tail?' I asked, 'the same
as the one I chose?'
"'Yes, Jem; there were two barred-tailed ones, and I could never
tell them apart.'
"Well, then, of course I saw it all, and I ran off as hard as my
feet would carry me to this man Breckinridge; but he had sold the
lot at once, and not one word would he tell me as to where they
had gone. You heard him yourselves to-night. Well, he has always
answered me like that. My sister thinks that I am going mad.
Sometimes I think that I am myself. And now--and now I am myself
a branded thief, without ever having touched the wealth for which
I sold my character. God help me! God help me!" He burst into
convulsive sobbing, with his face buried in his hands.
There was a long silence, broken only by his heavy breathing and
by the measured tapping of Sherlock Holmes' finger-tips upon the
edge of the table. Then my friend rose and threw open the door.
"Get out!" said he.
"What, sir! Oh, Heaven bless you!"
"No more words. Get out!"
And no more words were needed. There was a rush, a clatter upon
the stairs, the bang of a door, and the crisp rattle of running
footfalls from the street.
"After all, Watson," said Holmes, reaching up his hand for his
clay pipe, "I am not retained by the police to supply their
deficiencies. If Horner were in danger it would be another thing;
but this fellow will not appear against him, and the case must
collapse. I suppose that I am commuting a felony, but it is just
possible that I am saving a soul. This fellow will not go wrong
again; he is too terribly frightened. Send him to gaol now, and
you make him a gaol-bird for life. Besides, it is the season of
forgiveness. Chance has put in our way a most singular and
whimsical problem, and its solution is its own reward. If you
will have the goodness to touch the bell, Doctor, we will begin
another investigation, in which, also a bird will be the chief
feature."
VIII. THE ADVENTURE OF THE SPECKLED BAND
On glancing over my notes of the seventy odd cases in which I
have during the last eight years studied the methods of my friend
Sherlock Holmes, I find many tragic, some comic, a large number
merely strange, but none commonplace; for, working as he did
rather for the love of his art than for the acquirement of
wealth, he refused to associate himself with any investigation
which did not tend towards the unusual, and even the fantastic.
Of all these varied cases, however, I cannot recall any which
presented more singular features than that which was associated
with the well-known Surrey family of the Roylotts of Stoke Moran.
The events in question occurred in the early days of my
association with Holmes, when we were sharing rooms as bachelors
in Baker Street. It is possible that I might have placed them
upon record before, but a promise of secrecy was made at the
time, from which I have only been freed during the last month by
the untimely death of the lady to whom the pledge was given. It
is perhaps as well that the facts should now come to light, for I
have reasons to know that there are widespread rumours as to the
death of Dr. Grimesby Roylott which tend to make the matter even
more terrible than the truth.
It was early in April in the year '83 that I woke one morning to
find Sherlock Holmes standing, fully dressed, by the side of my
bed. He was a late riser, as a rule, and as the clock on the
mantelpiece showed me that it was only a quarter-past seven, I
blinked up at him in some surprise, and perhaps just a little
resentment, for I was myself regular in my habits.
"Very sorry to knock you up, Watson," said he, "but it's the
common lot this morning. Mrs. Hudson has been knocked up, she
retorted upon me, and I on you."
"What is it, then--a fire?"
"No; a client. It seems that a young lady has arrived in a
considerable state of excitement, who insists upon seeing me. She
is waiting now in the sitting-room. Now, when young ladies wander
about the metropolis at this hour of the morning, and knock
sleepy people up out of their beds, I presume that it is
something very pressing which they have to communicate. Should it
prove to be an interesting case, you would, I am sure, wish to
follow it from the outset. I thought, at any rate, that I should
call you and give you the chance."
"My dear fellow, I would not miss it for anything."
I had no keener pleasure than in following Holmes in his
professional investigations, and in admiring the rapid
deductions, as swift as intuitions, and yet always founded on a
logical basis with which he unravelled the problems which were
submitted to him. I rapidly threw on my clothes and was ready in
a few minutes to accompany my friend down to the sitting-room. A
lady dressed in black and heavily veiled, who had been sitting in
the window, rose as we entered.
"Good-morning, madam," said Holmes cheerily. "My name is Sherlock
Holmes. This is my intimate friend and associate, Dr. Watson,
before whom you can speak as freely as before myself. Ha! I am
glad to see that Mrs. Hudson has had the good sense to light the
fire. Pray draw up to it, and I shall order you a cup of hot
coffee, for I observe that you are shivering."
"It is not cold which makes me shiver," said the woman in a low
voice, changing her seat as requested.
"What, then?"
"It is fear, Mr. Holmes. It is terror." She raised her veil as
she spoke, and we could see that she was indeed in a pitiable
state of agitation, her face all drawn and grey, with restless
frightened eyes, like those of some hunted animal. Her features
and figure were those of a woman of thirty, but her hair was shot
with premature grey, and her expression was weary and haggard.
Sherlock Holmes ran her over with one of his quick,
all-comprehensive glances.
"You must not fear," said he soothingly, bending forward and
patting her forearm. "We shall soon set matters right, I have no
doubt. You have come in by train this morning, I see."
"You know me, then?"
"No, but I observe the second half of a return ticket in the palm
of your left glove. You must have started early, and yet you had
a good drive in a dog-cart, along heavy roads, before you reached
the station."
The lady gave a violent start and stared in bewilderment at my
companion.
"There is no mystery, my dear madam," said he, smiling. "The left
arm of your jacket is spattered with mud in no less than seven
places. The marks are perfectly fresh. There is no vehicle save a
dog-cart which throws up mud in that way, and then only when you
sit on the left-hand side of the driver."
"Whatever your reasons may be, you are perfectly correct," said
she. "I started from home before six, reached Leatherhead at
twenty past, and came in by the first train to Waterloo. Sir, I
can stand this strain no longer; I shall go mad if it continues.
I have no one to turn to--none, save only one, who cares for me,
and he, poor fellow, can be of little aid. I have heard of you,
Mr. Holmes; I have heard of you from Mrs. Farintosh, whom you
helped in the hour of her sore need. It was from her that I had
your address. Oh, sir, do you not think that you could help me,
too, and at least throw a little light through the dense darkness
which surrounds me? At present it is out of my power to reward
you for your services, but in a month or six weeks I shall be
married, with the control of my own income, and then at least you
shall not find me ungrateful."
Holmes turned to his desk and, unlocking it, drew out a small
case-book, which he consulted.
"Farintosh," said he. "Ah yes, I recall the case; it was
concerned with an opal tiara. I think it was before your time,
Watson. I can only say, madam, that I shall be happy to devote
the same care to your case as I did to that of your friend. As to
reward, my profession is its own reward; but you are at liberty
to defray whatever expenses I may be put to, at the time which
suits you best. And now I beg that you will lay before us
everything that may help us in forming an opinion upon the
matter."
"Alas!" replied our visitor, "the very horror of my situation
lies in the fact that my fears are so vague, and my suspicions
depend so entirely upon small points, which might seem trivial to
another, that even he to whom of all others I have a right to
look for help and advice looks upon all that I tell him about it
as the fancies of a nervous woman. He does not say so, but I can
read it from his soothing answers and averted eyes. But I have
heard, Mr. Holmes, that you can see deeply into the manifold
wickedness of the human heart. You may advise me how to walk amid
the dangers which encompass me."
"I am all attention, madam."
"My name is Helen Stoner, and I am living with my stepfather, who
is the last survivor of one of the oldest Saxon families in
England, the Roylotts of Stoke Moran, on the western border of
Surrey."
Holmes nodded his head. "The name is familiar to me," said he.
"The family was at one time among the richest in England, and the
estates extended over the borders into Berkshire in the north,
and Hampshire in the west. In the last century, however, four
successive heirs were of a dissolute and wasteful disposition,
and the family ruin was eventually completed by a gambler in the
days of the Regency. Nothing was left save a few acres of ground,
and the two-hundred-year-old house, which is itself crushed under
a heavy mortgage. The last squire dragged out his existence
there, living the horrible life of an aristocratic pauper; but
his only son, my stepfather, seeing that he must adapt himself to
the new conditions, obtained an advance from a relative, which
enabled him to take a medical degree and went out to Calcutta,
where, by his professional skill and his force of character, he
established a large practice. In a fit of anger, however, caused
by some robberies which had been perpetrated in the house, he
beat his native butler to death and narrowly escaped a capital
sentence. As it was, he suffered a long term of imprisonment and
afterwards returned to England a morose and disappointed man.
"When Dr. Roylott was in India he married my mother, Mrs. Stoner,
the young widow of Major-General Stoner, of the Bengal Artillery.
My sister Julia and I were twins, and we were only two years old
at the time of my mother's re-marriage. She had a considerable
sum of money--not less than 1000 pounds a year--and this she
bequeathed to Dr. Roylott entirely while we resided with him,
with a provision that a certain annual sum should be allowed to
each of us in the event of our marriage. Shortly after our return
to England my mother died--she was killed eight years ago in a
railway accident near Crewe. Dr. Roylott then abandoned his
attempts to establish himself in practice in London and took us
to live with him in the old ancestral house at Stoke Moran. The
money which my mother had left was enough for all our wants, and
there seemed to be no obstacle to our happiness.
"But a terrible change came over our stepfather about this time.
Instead of making friends and exchanging visits with our
neighbours, who had at first been overjoyed to see a Roylott of
Stoke Moran back in the old family seat, he shut himself up in
his house and seldom came out save to indulge in ferocious
quarrels with whoever might cross his path. Violence of temper
approaching to mania has been hereditary in the men of the
family, and in my stepfather's case it had, I believe, been
intensified by his long residence in the tropics. A series of
disgraceful brawls took place, two of which ended in the
police-court, until at last he became the terror of the village,
and the folks would fly at his approach, for he is a man of
immense strength, and absolutely uncontrollable in his anger.
"Last week he hurled the local blacksmith over a parapet into a
stream, and it was only by paying over all the money which I
could gather together that I was able to avert another public
exposure. He had no friends at all save the wandering gipsies,
and he would give these vagabonds leave to encamp upon the few
acres of bramble-covered land which represent the family estate,
and would accept in return the hospitality of their tents,
wandering away with them sometimes for weeks on end. He has a
passion also for Indian animals, which are sent over to him by a
correspondent, and he has at this moment a cheetah and a baboon,
which wander freely over his grounds and are feared by the
villagers almost as much as their master.
"You can imagine from what I say that my poor sister Julia and I
had no great pleasure in our lives. No servant would stay with
us, and for a long time we did all the work of the house. She was
but thirty at the time of her death, and yet her hair had already
begun to whiten, even as mine has."
"Your sister is dead, then?"
"She died just two years ago, and it is of her death that I wish
to speak to you. You can understand that, living the life which I
have described, we were little likely to see anyone of our own
age and position. We had, however, an aunt, my mother's maiden
sister, Miss Honoria Westphail, who lives near Harrow, and we
were occasionally allowed to pay short visits at this lady's
house. Julia went there at Christmas two years ago, and met there
a half-pay major of marines, to whom she became engaged. My
stepfather learned of the engagement when my sister returned and
offered no objection to the marriage; but within a fortnight of
the day which had been fixed for the wedding, the terrible event
occurred which has deprived me of my only companion."
Sherlock Holmes had been leaning back in his chair with his eyes
closed and his head sunk in a cushion, but he half opened his
lids now and glanced across at his visitor.
"Pray be precise as to details," said he.
"It is easy for me to be so, for every event of that dreadful
time is seared into my memory. The manor-house is, as I have
already said, very old, and only one wing is now inhabited. The
bedrooms in this wing are on the ground floor, the sitting-rooms
being in the central block of the buildings. Of these bedrooms
the first is Dr. Roylott's, the second my sister's, and the third
my own. There is no communication between them, but they all open
out into the same corridor. Do I make myself plain?"
"Perfectly so."
"The windows of the three rooms open out upon the lawn. That
fatal night Dr. Roylott had gone to his room early, though we
knew that he had not retired to rest, for my sister was troubled
by the smell of the strong Indian cigars which it was his custom
to smoke. She left her room, therefore, and came into mine, where
she sat for some time, chatting about her approaching wedding. At
eleven o'clock she rose to leave me, but she paused at the door
and looked back.
"'Tell me, Helen,' said she, 'have you ever heard anyone whistle
in the dead of the night?'
"'Never,' said I.
"'I suppose that you could not possibly whistle, yourself, in
your sleep?'
"'Certainly not. But why?'
"'Because during the last few nights I have always, about three
in the morning, heard a low, clear whistle. I am a light sleeper,
and it has awakened me. I cannot tell where it came from--perhaps
from the next room, perhaps from the lawn. I thought that I would
just ask you whether you had heard it.'
"'No, I have not. It must be those wretched gipsies in the
plantation.'
"'Very likely. And yet if it were on the lawn, I wonder that you
did not hear it also.'
"'Ah, but I sleep more heavily than you.'
"'Well, it is of no great consequence, at any rate.' She smiled
back at me, closed my door, and a few moments later I heard her
key turn in the lock."
"Indeed," said Holmes. "Was it your custom always to lock
yourselves in at night?"
"Always."
"And why?"
"I think that I mentioned to you that the doctor kept a cheetah
and a baboon. We had no feeling of security unless our doors were
locked."
"Quite so. Pray proceed with your statement."
"I could not sleep that night. A vague feeling of impending
misfortune impressed me. My sister and I, you will recollect,
were twins, and you know how subtle are the links which bind two
souls which are so closely allied. It was a wild night. The wind
was howling outside, and the rain was beating and splashing
against the windows. Suddenly, amid all the hubbub of the gale,
there burst forth the wild scream of a terrified woman. I knew
that it was my sister's voice. I sprang from my bed, wrapped a
shawl round me, and rushed into the corridor. As I opened my door
I seemed to hear a low whistle, such as my sister described, and
a few moments later a clanging sound, as if a mass of metal had
fallen. As I ran down the passage, my sister's door was unlocked,
and revolved slowly upon its hinges. I stared at it
horror-stricken, not knowing what was about to issue from it. By
the light of the corridor-lamp I saw my sister appear at the
opening, her face blanched with terror, her hands groping for
help, her whole figure swaying to and fro like that of a
drunkard. I ran to her and threw my arms round her, but at that
moment her knees seemed to give way and she fell to the ground.
She writhed as one who is in terrible pain, and her limbs were
dreadfully convulsed. At first I thought that she had not
recognised me, but as I bent over her she suddenly shrieked out
in a voice which I shall never forget, 'Oh, my God! Helen! It was
the band! The speckled band!' There was something else which she
would fain have said, and she stabbed with her finger into the
air in the direction of the doctor's room, but a fresh convulsion
seized her and choked her words. I rushed out, calling loudly for
my stepfather, and I met him hastening from his room in his
dressing-gown. When he reached my sister's side she was
unconscious, and though he poured brandy down her throat and sent
for medical aid from the village, all efforts were in vain, for
she slowly sank and died without having recovered her
consciousness. Such was the dreadful end of my beloved sister."
"One moment," said Holmes, "are you sure about this whistle and
metallic sound? Could you swear to it?"
"That was what the county coroner asked me at the inquiry. It is
my strong impression that I heard it, and yet, among the crash of
the gale and the creaking of an old house, I may possibly have
been deceived."
"Was your sister dressed?"
"No, she was in her night-dress. In her right hand was found the
charred stump of a match, and in her left a match-box."
"Showing that she had struck a light and looked about her when
the alarm took place. That is important. And what conclusions did
the coroner come to?"
"He investigated the case with great care, for Dr. Roylott's
conduct had long been notorious in the county, but he was unable
to find any satisfactory cause of death. My evidence showed that
the door had been fastened upon the inner side, and the windows
were blocked by old-fashioned shutters with broad iron bars,
which were secured every night. The walls were carefully sounded,
and were shown to be quite solid all round, and the flooring was
also thoroughly examined, with the same result. The chimney is
wide, but is barred up by four large staples. It is certain,
therefore, that my sister was quite alone when she met her end.
Besides, there were no marks of any violence upon her."
"How about poison?"
"The doctors examined her for it, but without success."
"What do you think that this unfortunate lady died of, then?"
"It is my belief that she died of pure fear and nervous shock,
though what it was that frightened her I cannot imagine."
"Were there gipsies in the plantation at the time?"
"Yes, there are nearly always some there."
"Ah, and what did you gather from this allusion to a band--a
speckled band?"
"Sometimes I have thought that it was merely the wild talk of
delirium, sometimes that it may have referred to some band of
people, perhaps to these very gipsies in the plantation. I do not
know whether the spotted handkerchiefs which so many of them wear
over their heads might have suggested the strange adjective which
she used."
Holmes shook his head like a man who is far from being satisfied.
"These are very deep waters," said he; "pray go on with your
narrative."
"Two years have passed since then, and my life has been until
lately lonelier than ever. A month ago, however, a dear friend,
whom I have known for many years, has done me the honour to ask
my hand in marriage. His name is Armitage--Percy Armitage--the
second son of Mr. Armitage, of Crane Water, near Reading. My
stepfather has offered no opposition to the match, and we are to
be married in the course of the spring. Two days ago some repairs
were started in the west wing of the building, and my bedroom
wall has been pierced, so that I have had to move into the
chamber in which my sister died, and to sleep in the very bed in
which she slept. Imagine, then, my thrill of terror when last
night, as I lay awake, thinking over her terrible fate, I
suddenly heard in the silence of the night the low whistle which
had been the herald of her own death. I sprang up and lit the
lamp, but nothing was to be seen in the room. I was too shaken to
go to bed again, however, so I dressed, and as soon as it was
daylight I slipped down, got a dog-cart at the Crown Inn, which
is opposite, and drove to Leatherhead, from whence I have come on
this morning with the one object of seeing you and asking your
advice."
"You have done wisely," said my friend. "But have you told me
all?"
"Yes, all."
"Miss Roylott, you have not. You are screening your stepfather."
"Why, what do you mean?"
For answer Holmes pushed back the frill of black lace which
fringed the hand that lay upon our visitor's knee. Five little
livid spots, the marks of four fingers and a thumb, were printed
upon the white wrist.
"You have been cruelly used," said Holmes.
The lady coloured deeply and covered over her injured wrist. "He
is a hard man," she said, "and perhaps he hardly knows his own
strength."
There was a long silence, during which Holmes leaned his chin
upon his hands and stared into the crackling fire.
"This is a very deep business," he said at last. "There are a
thousand details which I should desire to know before I decide
upon our course of action. Yet we have not a moment to lose. If
we were to come to Stoke Moran to-day, would it be possible for
us to see over these rooms without the knowledge of your
stepfather?"
"As it happens, he spoke of coming into town to-day upon some
most important business. It is probable that he will be away all
day, and that there would be nothing to disturb you. We have a
housekeeper now, but she is old and foolish, and I could easily
get her out of the way."
"Excellent. You are not averse to this trip, Watson?"
"By no means."
"Then we shall both come. What are you going to do yourself?"
"I have one or two things which I would wish to do now that I am
in town. But I shall return by the twelve o'clock train, so as to
be there in time for your coming."
"And you may expect us early in the afternoon. I have myself some
small business matters to attend to. Will you not wait and
breakfast?"
"No, I must go. My heart is lightened already since I have
confided my trouble to you. I shall look forward to seeing you
again this afternoon." She dropped her thick black veil over her
face and glided from the room.
"And what do you think of it all, Watson?" asked Sherlock Holmes,
leaning back in his chair.
"It seems to me to be a most dark and sinister business."
"Dark enough and sinister enough."
"Yet if the lady is correct in saying that the flooring and walls
are sound, and that the door, window, and chimney are impassable,
then her sister must have been undoubtedly alone when she met her
mysterious end."
"What becomes, then, of these nocturnal whistles, and what of the
very peculiar words of the dying woman?"
"I cannot think."
"When you combine the ideas of whistles at night, the presence of
a band of gipsies who are on intimate terms with this old doctor,
the fact that we have every reason to believe that the doctor has
an interest in preventing his stepdaughter's marriage, the dying
allusion to a band, and, finally, the fact that Miss Helen Stoner
heard a metallic clang, which might have been caused by one of
those metal bars that secured the shutters falling back into its
place, I think that there is good ground to think that the
mystery may be cleared along those lines."
"But what, then, did the gipsies do?"
"I cannot imagine."
"I see many objections to any such theory."
"And so do I. It is precisely for that reason that we are going
to Stoke Moran this day. I want to see whether the objections are
fatal, or if they may be explained away. But what in the name of
the devil!"
The ejaculation had been drawn from my companion by the fact that
our door had been suddenly dashed open, and that a huge man had
framed himself in the aperture. His costume was a peculiar
mixture of the professional and of the agricultural, having a
black top-hat, a long frock-coat, and a pair of high gaiters,
with a hunting-crop swinging in his hand. So tall was he that his
hat actually brushed the cross bar of the doorway, and his
breadth seemed to span it across from side to side. A large face,
seared with a thousand wrinkles, burned yellow with the sun, and
marked with every evil passion, was turned from one to the other
of us, while his deep-set, bile-shot eyes, and his high, thin,
fleshless nose, gave him somewhat the resemblance to a fierce old
bird of prey.
"Which of you is Holmes?" asked this apparition.
"My name, sir; but you have the advantage of me," said my
companion quietly.
"I am Dr. Grimesby Roylott, of Stoke Moran."
"Indeed, Doctor," said Holmes blandly. "Pray take a seat."
"I will do nothing of the kind. My stepdaughter has been here. I
have traced her. What has she been saying to you?"
"It is a little cold for the time of the year," said Holmes.
"What has she been saying to you?" screamed the old man
furiously.
"But I have heard that the crocuses promise well," continued my
companion imperturbably.
"Ha! You put me off, do you?" said our new visitor, taking a step
forward and shaking his hunting-crop. "I know you, you scoundrel!
I have heard of you before. You are Holmes, the meddler."
My friend smiled.
"Holmes, the busybody!"
His smile broadened.
"Holmes, the Scotland Yard Jack-in-office!"
Holmes chuckled heartily. "Your conversation is most
entertaining," said he. "When you go out close the door, for
there is a decided draught."
"I will go when I have said my say. Don't you dare to meddle with
my affairs. I know that Miss Stoner has been here. I traced her!
I am a dangerous man to fall foul of! See here." He stepped
swiftly forward, seized the poker, and bent it into a curve with
his huge brown hands.
"See that you keep yourself out of my grip," he snarled, and
hurling the twisted poker into the fireplace he strode out of the
room.
"He seems a very amiable person," said Holmes, laughing. "I am
not quite so bulky, but if he had remained I might have shown him
that my grip was not much more feeble than his own." As he spoke
he picked up the steel poker and, with a sudden effort,
straightened it out again.
"Fancy his having the insolence to confound me with the official
detective force! This incident gives zest to our investigation,
however, and I only trust that our little friend will not suffer
from her imprudence in allowing this brute to trace her. And now,
Watson, we shall order breakfast, and afterwards I shall walk
down to Doctors' Commons, where I hope to get some data which may
help us in this matter."
It was nearly one o'clock when Sherlock Holmes returned from his
excursion. He held in his hand a sheet of blue paper, scrawled
over with notes and figures.
"I have seen the will of the deceased wife," said he. "To
determine its exact meaning I have been obliged to work out the
present prices of the investments with which it is concerned. The
total income, which at the time of the wife's death was little
short of 1100 pounds, is now, through the fall in agricultural
prices, not more than 750 pounds. Each daughter can claim an
income of 250 pounds, in case of marriage. It is evident,
therefore, that if both girls had married, this beauty would have
had a mere pittance, while even one of them would cripple him to
a very serious extent. My morning's work has not been wasted,
since it has proved that he has the very strongest motives for
standing in the way of anything of the sort. And now, Watson,
this is too serious for dawdling, especially as the old man is
aware that we are interesting ourselves in his affairs; so if you
are ready, we shall call a cab and drive to Waterloo. I should be
very much obliged if you would slip your revolver into your
pocket. An Eley's No. 2 is an excellent argument with gentlemen
who can twist steel pokers into knots. That and a tooth-brush
are, I think, all that we need."
At Waterloo we were fortunate in catching a train for
Leatherhead, where we hired a trap at the station inn and drove
for four or five miles through the lovely Surrey lanes. It was a
perfect day, with a bright sun and a few fleecy clouds in the
heavens. The trees and wayside hedges were just throwing out
their first green shoots, and the air was full of the pleasant
smell of the moist earth. To me at least there was a strange
contrast between the sweet promise of the spring and this
sinister quest upon which we were engaged. My companion sat in
the front of the trap, his arms folded, his hat pulled down over
his eyes, and his chin sunk upon his breast, buried in the
deepest thought. Suddenly, however, he started, tapped me on the
shoulder, and pointed over the meadows.
"Look there!" said he.
A heavily timbered park stretched up in a gentle slope,
thickening into a grove at the highest point. From amid the
branches there jutted out the grey gables and high roof-tree of a
very old mansion.
"Stoke Moran?" said he.
"Yes, sir, that be the house of Dr. Grimesby Roylott," remarked
the driver.
"There is some building going on there," said Holmes; "that is
where we are going."
"There's the village," said the driver, pointing to a cluster of
roofs some distance to the left; "but if you want to get to the
house, you'll find it shorter to get over this stile, and so by
the foot-path over the fields. There it is, where the lady is
walking."
"And the lady, I fancy, is Miss Stoner," observed Holmes, shading
his eyes. "Yes, I think we had better do as you suggest."
We got off, paid our fare, and the trap rattled back on its way
to Leatherhead.
"I thought it as well," said Holmes as we climbed the stile,
"that this fellow should think we had come here as architects, or
on some definite business. It may stop his gossip.
Good-afternoon, Miss Stoner. You see that we have been as good as
our word."
Our client of the morning had hurried forward to meet us with a
face which spoke her joy. "I have been waiting so eagerly for
you," she cried, shaking hands with us warmly. "All has turned
out splendidly. Dr. Roylott has gone to town, and it is unlikely
that he will be back before evening."
"We have had the pleasure of making the doctor's acquaintance,"
said Holmes, and in a few words he sketched out what had
occurred. Miss Stoner turned white to the lips as she listened.
"Good heavens!" she cried, "he has followed me, then."
"So it appears."
"He is so cunning that I never know when I am safe from him. What
will he say when he returns?"
"He must guard himself, for he may find that there is someone
more cunning than himself upon his track. You must lock yourself
up from him to-night. If he is violent, we shall take you away to
your aunt's at Harrow. Now, we must make the best use of our
time, so kindly take us at once to the rooms which we are to
examine."
The building was of grey, lichen-blotched stone, with a high
central portion and two curving wings, like the claws of a crab,
thrown out on each side. In one of these wings the windows were
broken and blocked with wooden boards, while the roof was partly
caved in, a picture of ruin. The central portion was in little
better repair, but the right-hand block was comparatively modern,
and the blinds in the windows, with the blue smoke curling up
from the chimneys, showed that this was where the family resided.
Some scaffolding had been erected against the end wall, and the
stone-work had been broken into, but there were no signs of any
workmen at the moment of our visit. Holmes walked slowly up and
down the ill-trimmed lawn and examined with deep attention the
outsides of the windows.
"This, I take it, belongs to the room in which you used to sleep,
the centre one to your sister's, and the one next to the main
building to Dr. Roylott's chamber?"
"Exactly so. But I am now sleeping in the middle one."
"Pending the alterations, as I understand. By the way, there does
not seem to be any very pressing need for repairs at that end
wall."
"There were none. I believe that it was an excuse to move me from
my room."
"Ah! that is suggestive. Now, on the other side of this narrow
wing runs the corridor from which these three rooms open. There
are windows in it, of course?"
"Yes, but very small ones. Too narrow for anyone to pass
through."
"As you both locked your doors at night, your rooms were
unapproachable from that side. Now, would you have the kindness
to go into your room and bar your shutters?"
Miss Stoner did so, and Holmes, after a careful examination
through the open window, endeavoured in every way to force the
shutter open, but without success. There was no slit through
which a knife could be passed to raise the bar. Then with his
lens he tested the hinges, but they were of solid iron, built
firmly into the massive masonry. "Hum!" said he, scratching his
chin in some perplexity, "my theory certainly presents some
difficulties. No one could pass these shutters if they were
bolted. Well, we shall see if the inside throws any light upon
the matter."
A small side door led into the whitewashed corridor from which
the three bedrooms opened. Holmes refused to examine the third
chamber, so we passed at once to the second, that in which Miss
Stoner was now sleeping, and in which her sister had met with her
fate. It was a homely little room, with a low ceiling and a
gaping fireplace, after the fashion of old country-houses. A
brown chest of drawers stood in one corner, a narrow
white-counterpaned bed in another, and a dressing-table on the
left-hand side of the window. These articles, with two small
wicker-work chairs, made up all the furniture in the room save
for a square of Wilton carpet in the centre. The boards round and
the panelling of the walls were of brown, worm-eaten oak, so old
and discoloured that it may have dated from the original building
of the house. Holmes drew one of the chairs into a corner and sat
silent, while his eyes travelled round and round and up and down,
taking in every detail of the apartment.
"Where does that bell communicate with?" he asked at last
pointing to a thick bell-rope which hung down beside the bed, the
tassel actually lying upon the pillow.
"It goes to the housekeeper's room."
"It looks newer than the other things?"
"Yes, it was only put there a couple of years ago."
"Your sister asked for it, I suppose?"
"No, I never heard of her using it. We used always to get what we
wanted for ourselves."
"Indeed, it seemed unnecessary to put so nice a bell-pull there.
You will excuse me for a few minutes while I satisfy myself as to
this floor." He threw himself down upon his face with his lens in
his hand and crawled swiftly backward and forward, examining
minutely the cracks between the boards. Then he did the same with
the wood-work with which the chamber was panelled. Finally he
walked over to the bed and spent some time in staring at it and
in running his eye up and down the wall. Finally he took the
bell-rope in his hand and gave it a brisk tug.
"Why, it's a dummy," said he.
"Won't it ring?"
"No, it is not even attached to a wire. This is very interesting.
You can see now that it is fastened to a hook just above where
the little opening for the ventilator is."
"How very absurd! I never noticed that before."
"Very strange!" muttered Holmes, pulling at the rope. "There are
one or two very singular points about this room. For example,
what a fool a builder must be to open a ventilator into another
room, when, with the same trouble, he might have communicated
with the outside air!"
"That is also quite modern," said the lady.
"Done about the same time as the bell-rope?" remarked Holmes.
"Yes, there were several little changes carried out about that
time."
"They seem to have been of a most interesting character--dummy
bell-ropes, and ventilators which do not ventilate. With your
permission, Miss Stoner, we shall now carry our researches into
the inner apartment."
Dr. Grimesby Roylott's chamber was larger than that of his
step-daughter, but was as plainly furnished. A camp-bed, a small
wooden shelf full of books, mostly of a technical character, an
armchair beside the bed, a plain wooden chair against the wall, a
round table, and a large iron safe were the principal things
which met the eye. Holmes walked slowly round and examined each
and all of them with the keenest interest.
"What's in here?" he asked, tapping the safe.
"My stepfather's business papers."
"Oh! you have seen inside, then?"
"Only once, some years ago. I remember that it was full of
papers."
"There isn't a cat in it, for example?"
"No. What a strange idea!"
"Well, look at this!" He took up a small saucer of milk which
stood on the top of it.
"No; we don't keep a cat. But there is a cheetah and a baboon."
"Ah, yes, of course! Well, a cheetah is just a big cat, and yet a
saucer of milk does not go very far in satisfying its wants, I
daresay. There is one point which I should wish to determine." He
squatted down in front of the wooden chair and examined the seat
of it with the greatest attention.
"Thank you. That is quite settled," said he, rising and putting
his lens in his pocket. "Hullo! Here is something interesting!"
The object which had caught his eye was a small dog lash hung on
one corner of the bed. The lash, however, was curled upon itself
and tied so as to make a loop of whipcord.
"What do you make of that, Watson?"
"It's a common enough lash. But I don't know why it should be
tied."
"That is not quite so common, is it? Ah, me! it's a wicked world,
and when a clever man turns his brains to crime it is the worst
of all. I think that I have seen enough now, Miss Stoner, and
with your permission we shall walk out upon the lawn."
I had never seen my friend's face so grim or his brow so dark as
it was when we turned from the scene of this investigation. We
had walked several times up and down the lawn, neither Miss
Stoner nor myself liking to break in upon his thoughts before he
roused himself from his reverie.
"It is very essential, Miss Stoner," said he, "that you should
absolutely follow my advice in every respect."
"I shall most certainly do so."
"The matter is too serious for any hesitation. Your life may
depend upon your compliance."
"I assure you that I am in your hands."
"In the first place, both my friend and I must spend the night in
your room."
Both Miss Stoner and I gazed at him in astonishment.
"Yes, it must be so. Let me explain. I believe that that is the
village inn over there?"
"Yes, that is the Crown."
"Very good. Your windows would be visible from there?"
"Certainly."
"You must confine yourself to your room, on pretence of a
headache, when your stepfather comes back. Then when you hear him
retire for the night, you must open the shutters of your window,
undo the hasp, put your lamp there as a signal to us, and then
withdraw quietly with everything which you are likely to want
into the room which you used to occupy. I have no doubt that, in
spite of the repairs, you could manage there for one night."
"Oh, yes, easily."
"The rest you will leave in our hands."
"But what will you do?"
"We shall spend the night in your room, and we shall investigate
the cause of this noise which has disturbed you."
"I believe, Mr. Holmes, that you have already made up your mind,"
said Miss Stoner, laying her hand upon my companion's sleeve.
"Perhaps I have."
"Then, for pity's sake, tell me what was the cause of my sister's
death."
"I should prefer to have clearer proofs before I speak."
"You can at least tell me whether my own thought is correct, and
if she died from some sudden fright."
"No, I do not think so. I think that there was probably some more
tangible cause. And now, Miss Stoner, we must leave you for if
Dr. Roylott returned and saw us our journey would be in vain.
Good-bye, and be brave, for if you will do what I have told you,
you may rest assured that we shall soon drive away the dangers
that threaten you."
Sherlock Holmes and I had no difficulty in engaging a bedroom and
sitting-room at the Crown Inn. They were on the upper floor, and
from our window we could command a view of the avenue gate, and
of the inhabited wing of Stoke Moran Manor House. At dusk we saw
Dr. Grimesby Roylott drive past, his huge form looming up beside
the little figure of the lad who drove him. The boy had some
slight difficulty in undoing the heavy iron gates, and we heard
the hoarse roar of the doctor's voice and saw the fury with which
he shook his clinched fists at him. The trap drove on, and a few
minutes later we saw a sudden light spring up among the trees as
the lamp was lit in one of the sitting-rooms.
"Do you know, Watson," said Holmes as we sat together in the
gathering darkness, "I have really some scruples as to taking you
to-night. There is a distinct element of danger."
"Can I be of assistance?"
"Your presence might be invaluable."
"Then I shall certainly come."
"It is very kind of you."
"You speak of danger. You have evidently seen more in these rooms
than was visible to me."
"No, but I fancy that I may have deduced a little more. I imagine
that you saw all that I did."
"I saw nothing remarkable save the bell-rope, and what purpose
that could answer I confess is more than I can imagine."
"You saw the ventilator, too?"
"Yes, but I do not think that it is such a very unusual thing to
have a small opening between two rooms. It was so small that a
rat could hardly pass through."
"I knew that we should find a ventilator before ever we came to
Stoke Moran."
"My dear Holmes!"
"Oh, yes, I did. You remember in her statement she said that her
sister could smell Dr. Roylott's cigar. Now, of course that
suggested at once that there must be a communication between the
two rooms. It could only be a small one, or it would have been
remarked upon at the coroner's inquiry. I deduced a ventilator."
"But what harm can there be in that?"
"Well, there is at least a curious coincidence of dates. A
ventilator is made, a cord is hung, and a lady who sleeps in the
bed dies. Does not that strike you?"
"I cannot as yet see any connection."
"Did you observe anything very peculiar about that bed?"
"No."
"It was clamped to the floor. Did you ever see a bed fastened
like that before?"
"I cannot say that I have."
"The lady could not move her bed. It must always be in the same
relative position to the ventilator and to the rope--or so we may
call it, since it was clearly never meant for a bell-pull."
"Holmes," I cried, "I seem to see dimly what you are hinting at.
We are only just in time to prevent some subtle and horrible
crime."
"Subtle enough and horrible enough. When a doctor does go wrong
he is the first of criminals. He has nerve and he has knowledge.
Palmer and Pritchard were among the heads of their profession.
This man strikes even deeper, but I think, Watson, that we shall
be able to strike deeper still. But we shall have horrors enough
before the night is over; for goodness' sake let us have a quiet
pipe and turn our minds for a few hours to something more
cheerful."
About nine o'clock the light among the trees was extinguished,
and all was dark in the direction of the Manor House. Two hours
passed slowly away, and then, suddenly, just at the stroke of
eleven, a single bright light shone out right in front of us.
"That is our signal," said Holmes, springing to his feet; "it
comes from the middle window."
As we passed out he exchanged a few words with the landlord,
explaining that we were going on a late visit to an acquaintance,
and that it was possible that we might spend the night there. A
moment later we were out on the dark road, a chill wind blowing
in our faces, and one yellow light twinkling in front of us
through the gloom to guide us on our sombre errand.
There was little difficulty in entering the grounds, for
unrepaired breaches gaped in the old park wall. Making our way
among the trees, we reached the lawn, crossed it, and were about
to enter through the window when out from a clump of laurel
bushes there darted what seemed to be a hideous and distorted
child, who threw itself upon the grass with writhing limbs and
then ran swiftly across the lawn into the darkness.
"My God!" I whispered; "did you see it?"
Holmes was for the moment as startled as I. His hand closed like
a vice upon my wrist in his agitation. Then he broke into a low
laugh and put his lips to my ear.
"It is a nice household," he murmured. "That is the baboon."
I had forgotten the strange pets which the doctor affected. There
was a cheetah, too; perhaps we might find it upon our shoulders
at any moment. I confess that I felt easier in my mind when,
after following Holmes' example and slipping off my shoes, I
found myself inside the bedroom. My companion noiselessly closed
the shutters, moved the lamp onto the table, and cast his eyes
round the room. All was as we had seen it in the daytime. Then
creeping up to me and making a trumpet of his hand, he whispered
into my ear again so gently that it was all that I could do to
distinguish the words:
"The least sound would be fatal to our plans."
I nodded to show that I had heard.
"We must sit without light. He would see it through the
ventilator."
I nodded again.
"Do not go asleep; your very life may depend upon it. Have your
pistol ready in case we should need it. I will sit on the side of
the bed, and you in that chair."
I took out my revolver and laid it on the corner of the table.
Holmes had brought up a long thin cane, and this he placed upon
the bed beside him. By it he laid the box of matches and the
stump of a candle. Then he turned down the lamp, and we were left
in darkness.
How shall I ever forget that dreadful vigil? I could not hear a
sound, not even the drawing of a breath, and yet I knew that my
companion sat open-eyed, within a few feet of me, in the same
state of nervous tension in which I was myself. The shutters cut
off the least ray of light, and we waited in absolute darkness.
From outside came the occasional cry of a night-bird, and once at
our very window a long drawn catlike whine, which told us that
the cheetah was indeed at liberty. Far away we could hear the
deep tones of the parish clock, which boomed out every quarter of
an hour. How long they seemed, those quarters! Twelve struck, and
one and two and three, and still we sat waiting silently for
whatever might befall.
Suddenly there was the momentary gleam of a light up in the
direction of the ventilator, which vanished immediately, but was
succeeded by a strong smell of burning oil and heated metal.
Someone in the next room had lit a dark-lantern. I heard a gentle
sound of movement, and then all was silent once more, though the
smell grew stronger. For half an hour I sat with straining ears.
Then suddenly another sound became audible--a very gentle,
soothing sound, like that of a small jet of steam escaping
continually from a kettle. The instant that we heard it, Holmes
sprang from the bed, struck a match, and lashed furiously with
his cane at the bell-pull.
"You see it, Watson?" he yelled. "You see it?"
But I saw nothing. At the moment when Holmes struck the light I
heard a low, clear whistle, but the sudden glare flashing into my
weary eyes made it impossible for me to tell what it was at which
my friend lashed so savagely. I could, however, see that his face
was deadly pale and filled with horror and loathing. He had
ceased to strike and was gazing up at the ventilator when
suddenly there broke from the silence of the night the most
horrible cry to which I have ever listened. It swelled up louder
and louder, a hoarse yell of pain and fear and anger all mingled
in the one dreadful shriek. They say that away down in the
village, and even in the distant parsonage, that cry raised the
sleepers from their beds. It struck cold to our hearts, and I
stood gazing at Holmes, and he at me, until the last echoes of it
had died away into the silence from which it rose.
"What can it mean?" I gasped.
"It means that it is all over," Holmes answered. "And perhaps,
after all, it is for the best. Take your pistol, and we will
enter Dr. Roylott's room."
With a grave face he lit the lamp and led the way down the
corridor. Twice he struck at the chamber door without any reply
from within. Then he turned the handle and entered, I at his
heels, with the cocked pistol in my hand.
It was a singular sight which met our eyes. On the table stood a
dark-lantern with the shutter half open, throwing a brilliant
beam of light upon the iron safe, the door of which was ajar.
Beside this table, on the wooden chair, sat Dr. Grimesby Roylott
clad in a long grey dressing-gown, his bare ankles protruding
beneath, and his feet thrust into red heelless Turkish slippers.
Across his lap lay the short stock with the long lash which we
had noticed during the day. His chin was cocked upward and his
eyes were fixed in a dreadful, rigid stare at the corner of the
ceiling. Round his brow he had a peculiar yellow band, with
brownish speckles, which seemed to be bound tightly round his
head. As we entered he made neither sound nor motion.
"The band! the speckled band!" whispered Holmes.
I took a step forward. In an instant his strange headgear began
to move, and there reared itself from among his hair the squat
diamond-shaped head and puffed neck of a loathsome serpent.
"It is a swamp adder!" cried Holmes; "the deadliest snake in
India. He has died within ten seconds of being bitten. Violence
does, in truth, recoil upon the violent, and the schemer falls
into the pit which he digs for another. Let us thrust this
creature back into its den, and we can then remove Miss Stoner to
some place of shelter and let the county police know what has
happened."
As he spoke he drew the dog-whip swiftly from the dead man's lap,
and throwing the noose round the reptile's neck he drew it from
its horrid perch and, carrying it at arm's length, threw it into
the iron safe, which he closed upon it.
Such are the true facts of the death of Dr. Grimesby Roylott, of
Stoke Moran. It is not necessary that I should prolong a
narrative which has already run to too great a length by telling
how we broke the sad news to the terrified girl, how we conveyed
her by the morning train to the care of her good aunt at Harrow,
of how the slow process of official inquiry came to the
conclusion that the doctor met his fate while indiscreetly
playing with a dangerous pet. The little which I had yet to learn
of the case was told me by Sherlock Holmes as we travelled back
next day.
"I had," said he, "come to an entirely erroneous conclusion which
shows, my dear Watson, how dangerous it always is to reason from
insufficient data. The presence of the gipsies, and the use of
the word 'band,' which was used by the poor girl, no doubt, to
explain the appearance which she had caught a hurried glimpse of
by the light of her match, were sufficient to put me upon an
entirely wrong scent. I can only claim the merit that I instantly
reconsidered my position when, however, it became clear to me
that whatever danger threatened an occupant of the room could not
come either from the window or the door. My attention was
speedily drawn, as I have already remarked to you, to this
ventilator, and to the bell-rope which hung down to the bed. The
discovery that this was a dummy, and that the bed was clamped to
the floor, instantly gave rise to the suspicion that the rope was
there as a bridge for something passing through the hole and
coming to the bed. The idea of a snake instantly occurred to me,
and when I coupled it with my knowledge that the doctor was
furnished with a supply of creatures from India, I felt that I
was probably on the right track. The idea of using a form of
poison which could not possibly be discovered by any chemical
test was just such a one as would occur to a clever and ruthless
man who had had an Eastern training. The rapidity with which such
a poison would take effect would also, from his point of view, be
an advantage. It would be a sharp-eyed coroner, indeed, who could
distinguish the two little dark punctures which would show where
the poison fangs had done their work. Then I thought of the
whistle. Of course he must recall the snake before the morning
light revealed it to the victim. He had trained it, probably by
the use of the milk which we saw, to return to him when summoned.
He would put it through this ventilator at the hour that he
thought best, with the certainty that it would crawl down the
rope and land on the bed. It might or might not bite the
occupant, perhaps she might escape every night for a week, but
sooner or later she must fall a victim.
"I had come to these conclusions before ever I had entered his
room. An inspection of his chair showed me that he had been in
the habit of standing on it, which of course would be necessary
in order that he should reach the ventilator. The sight of the
safe, the saucer of milk, and the loop of whipcord were enough to
finally dispel any doubts which may have remained. The metallic
clang heard by Miss Stoner was obviously caused by her stepfather
hastily closing the door of his safe upon its terrible occupant.
Having once made up my mind, you know the steps which I took in
order to put the matter to the proof. I heard the creature hiss
as I have no doubt that you did also, and I instantly lit the
light and attacked it."
"With the result of driving it through the ventilator."
"And also with the result of causing it to turn upon its master
at the other side. Some of the blows of my cane came home and
roused its snakish temper, so that it flew upon the first person
it saw. In this way I am no doubt indirectly responsible for Dr.
Grimesby Roylott's death, and I cannot say that it is likely to
weigh very heavily upon my conscience."
IX. THE ADVENTURE OF THE ENGINEER'S THUMB
Of all the problems which have been submitted to my friend, Mr.
Sherlock Holmes, for solution during the years of our intimacy,
there were only two which I was the means of introducing to his
notice--that of Mr. Hatherley's thumb, and that of Colonel
Warburton's madness. Of these the latter may have afforded a
finer field for an acute and original observer, but the other was
so strange in its inception and so dramatic in its details that
it may be the more worthy of being placed upon record, even if it
gave my friend fewer openings for those deductive methods of
reasoning by which he achieved such remarkable results. The story
has, I believe, been told more than once in the newspapers, but,
like all such narratives, its effect is much less striking when
set forth en bloc in a single half-column of print than when the
facts slowly evolve before your own eyes, and the mystery clears
gradually away as each new discovery furnishes a step which leads
on to the complete truth. At the time the circumstances made a
deep impression upon me, and the lapse of two years has hardly
served to weaken the effect.
It was in the summer of '89, not long after my marriage, that the
events occurred which I am now about to summarise. I had returned
to civil practice and had finally abandoned Holmes in his Baker
Street rooms, although I continually visited him and occasionally
even persuaded him to forgo his Bohemian habits so far as to come
and visit us. My practice had steadily increased, and as I
happened to live at no very great distance from Paddington
Station, I got a few patients from among the officials. One of
these, whom I had cured of a painful and lingering disease, was
never weary of advertising my virtues and of endeavouring to send
me on every sufferer over whom he might have any influence.
One morning, at a little before seven o'clock, I was awakened by
the maid tapping at the door to announce that two men had come
from Paddington and were waiting in the consulting-room. I
dressed hurriedly, for I knew by experience that railway cases
were seldom trivial, and hastened downstairs. As I descended, my
old ally, the guard, came out of the room and closed the door
tightly behind him.
"I've got him here," he whispered, jerking his thumb over his
shoulder; "he's all right."
"What is it, then?" I asked, for his manner suggested that it was
some strange creature which he had caged up in my room.
"It's a new patient," he whispered. "I thought I'd bring him
round myself; then he couldn't slip away. There he is, all safe
and sound. I must go now, Doctor; I have my dooties, just the
same as you." And off he went, this trusty tout, without even
giving me time to thank him.
I entered my consulting-room and found a gentleman seated by the
table. He was quietly dressed in a suit of heather tweed with a
soft cloth cap which he had laid down upon my books. Round one of
his hands he had a handkerchief wrapped, which was mottled all
over with bloodstains. He was young, not more than
five-and-twenty, I should say, with a strong, masculine face; but
he was exceedingly pale and gave me the impression of a man who
was suffering from some strong agitation, which it took all his
strength of mind to control.
"I am sorry to knock you up so early, Doctor," said he, "but I
have had a very serious accident during the night. I came in by
train this morning, and on inquiring at Paddington as to where I
might find a doctor, a worthy fellow very kindly escorted me
here. I gave the maid a card, but I see that she has left it upon
the side-table."
I took it up and glanced at it. "Mr. Victor Hatherley, hydraulic
engineer, 16A, Victoria Street (3rd floor)." That was the name,
style, and abode of my morning visitor. "I regret that I have
kept you waiting," said I, sitting down in my library-chair. "You
are fresh from a night journey, I understand, which is in itself
a monotonous occupation."
"Oh, my night could not be called monotonous," said he, and
laughed. He laughed very heartily, with a high, ringing note,
leaning back in his chair and shaking his sides. All my medical
instincts rose up against that laugh.
"Stop it!" I cried; "pull yourself together!" and I poured out
some water from a caraffe.
It was useless, however. He was off in one of those hysterical
outbursts which come upon a strong nature when some great crisis
is over and gone. Presently he came to himself once more, very
weary and pale-looking.
"I have been making a fool of myself," he gasped.
"Not at all. Drink this." I dashed some brandy into the water,
and the colour began to come back to his bloodless cheeks.
"That's better!" said he. "And now, Doctor, perhaps you would
kindly attend to my thumb, or rather to the place where my thumb
used to be."
He unwound the handkerchief and held out his hand. It gave even
my hardened nerves a shudder to look at it. There were four
protruding fingers and a horrid red, spongy surface where the
thumb should have been. It had been hacked or torn right out from
the roots.
"Good heavens!" I cried, "this is a terrible injury. It must have
bled considerably."
"Yes, it did. I fainted when it was done, and I think that I must
have been senseless for a long time. When I came to I found that
it was still bleeding, so I tied one end of my handkerchief very
tightly round the wrist and braced it up with a twig."
"Excellent! You should have been a surgeon."
"It is a question of hydraulics, you see, and came within my own
province."
"This has been done," said I, examining the wound, "by a very
heavy and sharp instrument."
"A thing like a cleaver," said he.
"An accident, I presume?"
"By no means."
"What! a murderous attack?"
"Very murderous indeed."
"You horrify me."
I sponged the wound, cleaned it, dressed it, and finally covered
it over with cotton wadding and carbolised bandages. He lay back
without wincing, though he bit his lip from time to time.
"How is that?" I asked when I had finished.
"Capital! Between your brandy and your bandage, I feel a new man.
I was very weak, but I have had a good deal to go through."
"Perhaps you had better not speak of the matter. It is evidently
trying to your nerves."
"Oh, no, not now. I shall have to tell my tale to the police;
but, between ourselves, if it were not for the convincing
evidence of this wound of mine, I should be surprised if they
believed my statement, for it is a very extraordinary one, and I
have not much in the way of proof with which to back it up; and,
even if they believe me, the clues which I can give them are so
vague that it is a question whether justice will be done."
"Ha!" cried I, "if it is anything in the nature of a problem
which you desire to see solved, I should strongly recommend you
to come to my friend, Mr. Sherlock Holmes, before you go to the
official police."
"Oh, I have heard of that fellow," answered my visitor, "and I
should be very glad if he would take the matter up, though of
course I must use the official police as well. Would you give me
an introduction to him?"
"I'll do better. I'll take you round to him myself."
"I should be immensely obliged to you."
"We'll call a cab and go together. We shall just be in time to
have a little breakfast with him. Do you feel equal to it?"
"Yes; I shall not feel easy until I have told my story."
"Then my servant will call a cab, and I shall be with you in an
instant." I rushed upstairs, explained the matter shortly to my
wife, and in five minutes was inside a hansom, driving with my
new acquaintance to Baker Street.
Sherlock Holmes was, as I expected, lounging about his
sitting-room in his dressing-gown, reading the agony column of The
Times and smoking his before-breakfast pipe, which was composed
of all the plugs and dottles left from his smokes of the day
before, all carefully dried and collected on the corner of the
mantelpiece. He received us in his quietly genial fashion,
ordered fresh rashers and eggs, and joined us in a hearty meal.
When it was concluded he settled our new acquaintance upon the
sofa, placed a pillow beneath his head, and laid a glass of
brandy and water within his reach.
"It is easy to see that your experience has been no common one,
Mr. Hatherley," said he. "Pray, lie down there and make yourself
absolutely at home. Tell us what you can, but stop when you are
tired and keep up your strength with a little stimulant."
"Thank you," said my patient, "but I have felt another man since
the doctor bandaged me, and I think that your breakfast has
completed the cure. I shall take up as little of your valuable
time as possible, so I shall start at once upon my peculiar
experiences."
Holmes sat in his big armchair with the weary, heavy-lidded
expression which veiled his keen and eager nature, while I sat
opposite to him, and we listened in silence to the strange story
which our visitor detailed to us.
"You must know," said he, "that I am an orphan and a bachelor,
residing alone in lodgings in London. By profession I am a
hydraulic engineer, and I have had considerable experience of my
work during the seven years that I was apprenticed to Venner &
Matheson, the well-known firm, of Greenwich. Two years ago,
having served my time, and having also come into a fair sum of
money through my poor father's death, I determined to start in
business for myself and took professional chambers in Victoria
Street.
"I suppose that everyone finds his first independent start in
business a dreary experience. To me it has been exceptionally so.
During two years I have had three consultations and one small
job, and that is absolutely all that my profession has brought
me. My gross takings amount to 27 pounds 10s. Every day, from
nine in the morning until four in the afternoon, I waited in my
little den, until at last my heart began to sink, and I came to
believe that I should never have any practice at all.
"Yesterday, however, just as I was thinking of leaving the
office, my clerk entered to say there was a gentleman waiting who
wished to see me upon business. He brought up a card, too, with
the name of 'Colonel Lysander Stark' engraved upon it. Close at
his heels came the colonel himself, a man rather over the middle
size, but of an exceeding thinness. I do not think that I have
ever seen so thin a man. His whole face sharpened away into nose
and chin, and the skin of his cheeks was drawn quite tense over
his outstanding bones. Yet this emaciation seemed to be his
natural habit, and due to no disease, for his eye was bright, his
step brisk, and his bearing assured. He was plainly but neatly
dressed, and his age, I should judge, would be nearer forty than
thirty.
"'Mr. Hatherley?' said he, with something of a German accent.
'You have been recommended to me, Mr. Hatherley, as being a man
who is not only proficient in his profession but is also discreet
and capable of preserving a secret.'
"I bowed, feeling as flattered as any young man would at such an
address. 'May I ask who it was who gave me so good a character?'
"'Well, perhaps it is better that I should not tell you that just
at this moment. I have it from the same source that you are both
an orphan and a bachelor and are residing alone in London.'
"'That is quite correct,' I answered; 'but you will excuse me if
I say that I cannot see how all this bears upon my professional
qualifications. I understand that it was on a professional matter
that you wished to speak to me?'
"'Undoubtedly so. But you will find that all I say is really to
the point. I have a professional commission for you, but absolute
secrecy is quite essential--absolute secrecy, you understand, and
of course we may expect that more from a man who is alone than
from one who lives in the bosom of his family.'
"'If I promise to keep a secret,' said I, 'you may absolutely
depend upon my doing so.'
"He looked very hard at me as I spoke, and it seemed to me that I
had never seen so suspicious and questioning an eye.
"'Do you promise, then?' said he at last.
"'Yes, I promise.'
"'Absolute and complete silence before, during, and after? No
reference to the matter at all, either in word or writing?'
"'I have already given you my word.'
"'Very good.' He suddenly sprang up, and darting like lightning
across the room he flung open the door. The passage outside was
empty.
"'That's all right,' said he, coming back. 'I know that clerks are
sometimes curious as to their master's affairs. Now we can talk
in safety.' He drew up his chair very close to mine and began to
stare at me again with the same questioning and thoughtful look.
"A feeling of repulsion, and of something akin to fear had begun
to rise within me at the strange antics of this fleshless man.
Even my dread of losing a client could not restrain me from
showing my impatience.
"'I beg that you will state your business, sir,' said I; 'my time
is of value.' Heaven forgive me for that last sentence, but the
words came to my lips.
"'How would fifty guineas for a night's work suit you?' he asked.
"'Most admirably.'
"'I say a night's work, but an hour's would be nearer the mark. I
simply want your opinion about a hydraulic stamping machine which
has got out of gear. If you show us what is wrong we shall soon
set it right ourselves. What do you think of such a commission as
that?'
"'The work appears to be light and the pay munificent.'
"'Precisely so. We shall want you to come to-night by the last
train.'
"'Where to?'
"'To Eyford, in Berkshire. It is a little place near the borders
of Oxfordshire, and within seven miles of Reading. There is a
train from Paddington which would bring you there at about
11:15.'
"'Very good.'
"'I shall come down in a carriage to meet you.'
"'There is a drive, then?'
"'Yes, our little place is quite out in the country. It is a good
seven miles from Eyford Station.'
"'Then we can hardly get there before midnight. I suppose there
would be no chance of a train back. I should be compelled to stop
the night.'
"'Yes, we could easily give you a shake-down.'
"'That is very awkward. Could I not come at some more convenient
hour?'
"'We have judged it best that you should come late. It is to
recompense you for any inconvenience that we are paying to you, a
young and unknown man, a fee which would buy an opinion from the
very heads of your profession. Still, of course, if you would
like to draw out of the business, there is plenty of time to do
so.'
"I thought of the fifty guineas, and of how very useful they
would be to me. 'Not at all,' said I, 'I shall be very happy to
accommodate myself to your wishes. I should like, however, to
understand a little more clearly what it is that you wish me to
do.'
"'Quite so. It is very natural that the pledge of secrecy which
we have exacted from you should have aroused your curiosity. I
have no wish to commit you to anything without your having it all
laid before you. I suppose that we are absolutely safe from
eavesdroppers?'
"'Entirely.'
"'Then the matter stands thus. You are probably aware that
fuller's-earth is a valuable product, and that it is only found
in one or two places in England?'
"'I have heard so.'
"'Some little time ago I bought a small place--a very small
place--within ten miles of Reading. I was fortunate enough to
discover that there was a deposit of fuller's-earth in one of my
fields. On examining it, however, I found that this deposit was a
comparatively small one, and that it formed a link between two
very much larger ones upon the right and left--both of them,
however, in the grounds of my neighbours. These good people were
absolutely ignorant that their land contained that which was
quite as valuable as a gold-mine. Naturally, it was to my
interest to buy their land before they discovered its true value,
but unfortunately I had no capital by which I could do this. I
took a few of my friends into the secret, however, and they
suggested that we should quietly and secretly work our own little
deposit and that in this way we should earn the money which would
enable us to buy the neighbouring fields. This we have now been
doing for some time, and in order to help us in our operations we
erected a hydraulic press. This press, as I have already
explained, has got out of order, and we wish your advice upon the
subject. We guard our secret very jealously, however, and if it
once became known that we had hydraulic engineers coming to our
little house, it would soon rouse inquiry, and then, if the facts
came out, it would be good-bye to any chance of getting these
fields and carrying out our plans. That is why I have made you
promise me that you will not tell a human being that you are
going to Eyford to-night. I hope that I make it all plain?'
"'I quite follow you,' said I. 'The only point which I could not
quite understand was what use you could make of a hydraulic press
in excavating fuller's-earth, which, as I understand, is dug out
like gravel from a pit.'
"'Ah!' said he carelessly, 'we have our own process. We compress
the earth into bricks, so as to remove them without revealing
what they are. But that is a mere detail. I have taken you fully
into my confidence now, Mr. Hatherley, and I have shown you how I
trust you.' He rose as he spoke. 'I shall expect you, then, at
Eyford at 11:15.'
"'I shall certainly be there.'
"'And not a word to a soul.' He looked at me with a last long,
questioning gaze, and then, pressing my hand in a cold, dank
grasp, he hurried from the room.
"Well, when I came to think it all over in cool blood I was very
much astonished, as you may both think, at this sudden commission
which had been intrusted to me. On the one hand, of course, I was
glad, for the fee was at least tenfold what I should have asked
had I set a price upon my own services, and it was possible that
this order might lead to other ones. On the other hand, the face
and manner of my patron had made an unpleasant impression upon
me, and I could not think that his explanation of the
fuller's-earth was sufficient to explain the necessity for my
coming at midnight, and his extreme anxiety lest I should tell
anyone of my errand. However, I threw all fears to the winds, ate
a hearty supper, drove to Paddington, and started off, having
obeyed to the letter the injunction as to holding my tongue.
"At Reading I had to change not only my carriage but my station.
However, I was in time for the last train to Eyford, and I
reached the little dim-lit station after eleven o'clock. I was the
only passenger who got out there, and there was no one upon the
platform save a single sleepy porter with a lantern. As I passed
out through the wicket gate, however, I found my acquaintance of
the morning waiting in the shadow upon the other side. Without a
word he grasped my arm and hurried me into a carriage, the door
of which was standing open. He drew up the windows on either
side, tapped on the wood-work, and away we went as fast as the
horse could go."
"One horse?" interjected Holmes.
"Yes, only one."
"Did you observe the colour?"
"Yes, I saw it by the side-lights when I was stepping into the
carriage. It was a chestnut."
"Tired-looking or fresh?"
"Oh, fresh and glossy."
"Thank you. I am sorry to have interrupted you. Pray continue
your most interesting statement."
"Away we went then, and we drove for at least an hour. Colonel
Lysander Stark had said that it was only seven miles, but I
should think, from the rate that we seemed to go, and from the
time that we took, that it must have been nearer twelve. He sat
at my side in silence all the time, and I was aware, more than
once when I glanced in his direction, that he was looking at me
with great intensity. The country roads seem to be not very good
in that part of the world, for we lurched and jolted terribly. I
tried to look out of the windows to see something of where we
were, but they were made of frosted glass, and I could make out
nothing save the occasional bright blur of a passing light. Now
and then I hazarded some remark to break the monotony of the
journey, but the colonel answered only in monosyllables, and the
conversation soon flagged. At last, however, the bumping of the
road was exchanged for the crisp smoothness of a gravel-drive,
and the carriage came to a stand. Colonel Lysander Stark sprang
out, and, as I followed after him, pulled me swiftly into a porch
which gaped in front of us. We stepped, as it were, right out of
the carriage and into the hall, so that I failed to catch the
most fleeting glance of the front of the house. The instant that
I had crossed the threshold the door slammed heavily behind us,
and I heard faintly the rattle of the wheels as the carriage
drove away.
"It was pitch dark inside the house, and the colonel fumbled
about looking for matches and muttering under his breath.
Suddenly a door opened at the other end of the passage, and a
long, golden bar of light shot out in our direction. It grew
broader, and a woman appeared with a lamp in her hand, which she
held above her head, pushing her face forward and peering at us.
I could see that she was pretty, and from the gloss with which
the light shone upon her dark dress I knew that it was a rich
material. She spoke a few words in a foreign tongue in a tone as
though asking a question, and when my companion answered in a
gruff monosyllable she gave such a start that the lamp nearly
fell from her hand. Colonel Stark went up to her, whispered
something in her ear, and then, pushing her back into the room
from whence she had come, he walked towards me again with the
lamp in his hand.
"'Perhaps you will have the kindness to wait in this room for a
few minutes,' said he, throwing open another door. It was a
quiet, little, plainly furnished room, with a round table in the
centre, on which several German books were scattered. Colonel
Stark laid down the lamp on the top of a harmonium beside the
door. 'I shall not keep you waiting an instant,' said he, and
vanished into the darkness.
"I glanced at the books upon the table, and in spite of my
ignorance of German I could see that two of them were treatises
on science, the others being volumes of poetry. Then I walked
across to the window, hoping that I might catch some glimpse of
the country-side, but an oak shutter, heavily barred, was folded
across it. It was a wonderfully silent house. There was an old
clock ticking loudly somewhere in the passage, but otherwise
everything was deadly still. A vague feeling of uneasiness began
to steal over me. Who were these German people, and what were
they doing living in this strange, out-of-the-way place? And
where was the place? I was ten miles or so from Eyford, that was
all I knew, but whether north, south, east, or west I had no
idea. For that matter, Reading, and possibly other large towns,
were within that radius, so the place might not be so secluded,
after all. Yet it was quite certain, from the absolute stillness,
that we were in the country. I paced up and down the room,
humming a tune under my breath to keep up my spirits and feeling
that I was thoroughly earning my fifty-guinea fee.
"Suddenly, without any preliminary sound in the midst of the
utter stillness, the door of my room swung slowly open. The woman
was standing in the aperture, the darkness of the hall behind
her, the yellow light from my lamp beating upon her eager and
beautiful face. I could see at a glance that she was sick with
fear, and the sight sent a chill to my own heart. She held up one
shaking finger to warn me to be silent, and she shot a few
whispered words of broken English at me, her eyes glancing back,
like those of a frightened horse, into the gloom behind her.
"'I would go,' said she, trying hard, as it seemed to me, to
speak calmly; 'I would go. I should not stay here. There is no
good for you to do.'
"'But, madam,' said I, 'I have not yet done what I came for. I
cannot possibly leave until I have seen the machine.'
"'It is not worth your while to wait,' she went on. 'You can pass
through the door; no one hinders.' And then, seeing that I smiled
and shook my head, she suddenly threw aside her constraint and
made a step forward, with her hands wrung together. 'For the love
of Heaven!' she whispered, 'get away from here before it is too
late!'
"But I am somewhat headstrong by nature, and the more ready to
engage in an affair when there is some obstacle in the way. I
thought of my fifty-guinea fee, of my wearisome journey, and of
the unpleasant night which seemed to be before me. Was it all to
go for nothing? Why should I slink away without having carried
out my commission, and without the payment which was my due? This
woman might, for all I knew, be a monomaniac. With a stout
bearing, therefore, though her manner had shaken me more than I
cared to confess, I still shook my head and declared my intention
of remaining where I was. She was about to renew her entreaties
when a door slammed overhead, and the sound of several footsteps
was heard upon the stairs. She listened for an instant, threw up
her hands with a despairing gesture, and vanished as suddenly and
as noiselessly as she had come.
"The newcomers were Colonel Lysander Stark and a short thick man
with a chinchilla beard growing out of the creases of his double
chin, who was introduced to me as Mr. Ferguson.
"'This is my secretary and manager,' said the colonel. 'By the
way, I was under the impression that I left this door shut just
now. I fear that you have felt the draught.'
"'On the contrary,' said I, 'I opened the door myself because I
felt the room to be a little close.'
"He shot one of his suspicious looks at me. 'Perhaps we had
better proceed to business, then,' said he. 'Mr. Ferguson and I
will take you up to see the machine.'
"'I had better put my hat on, I suppose.'
"'Oh, no, it is in the house.'
"'What, you dig fuller's-earth in the house?'
"'No, no. This is only where we compress it. But never mind that.
All we wish you to do is to examine the machine and to let us
know what is wrong with it.'
"We went upstairs together, the colonel first with the lamp, the
fat manager and I behind him. It was a labyrinth of an old house,
with corridors, passages, narrow winding staircases, and little
low doors, the thresholds of which were hollowed out by the
generations who had crossed them. There were no carpets and no
signs of any furniture above the ground floor, while the plaster
was peeling off the walls, and the damp was breaking through in
green, unhealthy blotches. I tried to put on as unconcerned an
air as possible, but I had not forgotten the warnings of the
lady, even though I disregarded them, and I kept a keen eye upon
my two companions. Ferguson appeared to be a morose and silent
man, but I could see from the little that he said that he was at
least a fellow-countryman.
"Colonel Lysander Stark stopped at last before a low door, which
he unlocked. Within was a small, square room, in which the three
of us could hardly get at one time. Ferguson remained outside,
and the colonel ushered me in.
"'We are now,' said he, 'actually within the hydraulic press, and
it would be a particularly unpleasant thing for us if anyone were
to turn it on. The ceiling of this small chamber is really the
end of the descending piston, and it comes down with the force of
many tons upon this metal floor. There are small lateral columns
of water outside which receive the force, and which transmit and
multiply it in the manner which is familiar to you. The machine
goes readily enough, but there is some stiffness in the working
of it, and it has lost a little of its force. Perhaps you will
have the goodness to look it over and to show us how we can set
it right.'
"I took the lamp from him, and I examined the machine very
thoroughly. It was indeed a gigantic one, and capable of
exercising enormous pressure. When I passed outside, however, and
pressed down the levers which controlled it, I knew at once by
the whishing sound that there was a slight leakage, which allowed
a regurgitation of water through one of the side cylinders. An
examination showed that one of the india-rubber bands which was
round the head of a driving-rod had shrunk so as not quite to
fill the socket along which it worked. This was clearly the cause
of the loss of power, and I pointed it out to my companions, who
followed my remarks very carefully and asked several practical
questions as to how they should proceed to set it right. When I
had made it clear to them, I returned to the main chamber of the
machine and took a good look at it to satisfy my own curiosity.
It was obvious at a glance that the story of the fuller's-earth
was the merest fabrication, for it would be absurd to suppose
that so powerful an engine could be designed for so inadequate a
purpose. The walls were of wood, but the floor consisted of a
large iron trough, and when I came to examine it I could see a
crust of metallic deposit all over it. I had stooped and was
scraping at this to see exactly what it was when I heard a
muttered exclamation in German and saw the cadaverous face of the
colonel looking down at me.
"'What are you doing there?' he asked.
"I felt angry at having been tricked by so elaborate a story as
that which he had told me. 'I was admiring your fuller's-earth,'
said I; 'I think that I should be better able to advise you as to
your machine if I knew what the exact purpose was for which it
was used.'
"The instant that I uttered the words I regretted the rashness of
my speech. His face set hard, and a baleful light sprang up in
his grey eyes.
"'Very well,' said he, 'you shall know all about the machine.' He
took a step backward, slammed the little door, and turned the key
in the lock. I rushed towards it and pulled at the handle, but it
was quite secure, and did not give in the least to my kicks and
shoves. 'Hullo!' I yelled. 'Hullo! Colonel! Let me out!'
"And then suddenly in the silence I heard a sound which sent my
heart into my mouth. It was the clank of the levers and the swish
of the leaking cylinder. He had set the engine at work. The lamp
still stood upon the floor where I had placed it when examining
the trough. By its light I saw that the black ceiling was coming
down upon me, slowly, jerkily, but, as none knew better than
myself, with a force which must within a minute grind me to a
shapeless pulp. I threw myself, screaming, against the door, and
dragged with my nails at the lock. I implored the colonel to let
me out, but the remorseless clanking of the levers drowned my
cries. The ceiling was only a foot or two above my head, and with
my hand upraised I could feel its hard, rough surface. Then it
flashed through my mind that the pain of my death would depend
very much upon the position in which I met it. If I lay on my
face the weight would come upon my spine, and I shuddered to
think of that dreadful snap. Easier the other way, perhaps; and
yet, had I the nerve to lie and look up at that deadly black
shadow wavering down upon me? Already I was unable to stand
erect, when my eye caught something which brought a gush of hope
back to my heart.
"I have said that though the floor and ceiling were of iron, the
walls were of wood. As I gave a last hurried glance around, I saw
a thin line of yellow light between two of the boards, which
broadened and broadened as a small panel was pushed backward. For
an instant I could hardly believe that here was indeed a door
which led away from death. The next instant I threw myself
through, and lay half-fainting upon the other side. The panel had
closed again behind me, but the crash of the lamp, and a few
moments afterwards the clang of the two slabs of metal, told me
how narrow had been my escape.
"I was recalled to myself by a frantic plucking at my wrist, and
I found myself lying upon the stone floor of a narrow corridor,
while a woman bent over me and tugged at me with her left hand,
while she held a candle in her right. It was the same good friend
whose warning I had so foolishly rejected.
"'Come! come!' she cried breathlessly. 'They will be here in a
moment. They will see that you are not there. Oh, do not waste
the so-precious time, but come!'
"This time, at least, I did not scorn her advice. I staggered to
my feet and ran with her along the corridor and down a winding
stair. The latter led to another broad passage, and just as we
reached it we heard the sound of running feet and the shouting of
two voices, one answering the other from the floor on which we
were and from the one beneath. My guide stopped and looked about
her like one who is at her wit's end. Then she threw open a door
which led into a bedroom, through the window of which the moon
was shining brightly.
"'It is your only chance,' said she. 'It is high, but it may be
that you can jump it.'
"As she spoke a light sprang into view at the further end of the
passage, and I saw the lean figure of Colonel Lysander Stark
rushing forward with a lantern in one hand and a weapon like a
butcher's cleaver in the other. I rushed across the bedroom,
flung open the window, and looked out. How quiet and sweet and
wholesome the garden looked in the moonlight, and it could not be
more than thirty feet down. I clambered out upon the sill, but I
hesitated to jump until I should have heard what passed between
my saviour and the ruffian who pursued me. If she were ill-used,
then at any risks I was determined to go back to her assistance.
The thought had hardly flashed through my mind before he was at
the door, pushing his way past her; but she threw her arms round
him and tried to hold him back.
"'Fritz! Fritz!' she cried in English, 'remember your promise
after the last time. You said it should not be again. He will be
silent! Oh, he will be silent!'
"'You are mad, Elise!' he shouted, struggling to break away from
her. 'You will be the ruin of us. He has seen too much. Let me
pass, I say!' He dashed her to one side, and, rushing to the
window, cut at me with his heavy weapon. I had let myself go, and
was hanging by the hands to the sill, when his blow fell. I was
conscious of a dull pain, my grip loosened, and I fell into the
garden below.
"I was shaken but not hurt by the fall; so I picked myself up and
rushed off among the bushes as hard as I could run, for I
understood that I was far from being out of danger yet. Suddenly,
however, as I ran, a deadly dizziness and sickness came over me.
I glanced down at my hand, which was throbbing painfully, and
then, for the first time, saw that my thumb had been cut off and
that the blood was pouring from my wound. I endeavoured to tie my
handkerchief round it, but there came a sudden buzzing in my
ears, and next moment I fell in a dead faint among the
rose-bushes.
"How long I remained unconscious I cannot tell. It must have been
a very long time, for the moon had sunk, and a bright morning was
breaking when I came to myself. My clothes were all sodden with
dew, and my coat-sleeve was drenched with blood from my wounded
thumb. The smarting of it recalled in an instant all the
particulars of my night's adventure, and I sprang to my feet with
the feeling that I might hardly yet be safe from my pursuers. But
to my astonishment, when I came to look round me, neither house
nor garden were to be seen. I had been lying in an angle of the
hedge close by the highroad, and just a little lower down was a
long building, which proved, upon my approaching it, to be the
very station at which I had arrived upon the previous night. Were
it not for the ugly wound upon my hand, all that had passed
during those dreadful hours might have been an evil dream.
"Half dazed, I went into the station and asked about the morning
train. There would be one to Reading in less than an hour. The
same porter was on duty, I found, as had been there when I
arrived. I inquired of him whether he had ever heard of Colonel
Lysander Stark. The name was strange to him. Had he observed a
carriage the night before waiting for me? No, he had not. Was
there a police-station anywhere near? There was one about three
miles off.
"It was too far for me to go, weak and ill as I was. I determined
to wait until I got back to town before telling my story to the
police. It was a little past six when I arrived, so I went first
to have my wound dressed, and then the doctor was kind enough to
bring me along here. I put the case into your hands and shall do
exactly what you advise."
We both sat in silence for some little time after listening to
this extraordinary narrative. Then Sherlock Holmes pulled down
from the shelf one of the ponderous commonplace books in which he
placed his cuttings.
"Here is an advertisement which will interest you," said he. "It
appeared in all the papers about a year ago. Listen to this:
'Lost, on the 9th inst., Mr. Jeremiah Hayling, aged
twenty-six, a hydraulic engineer. Left his lodgings at ten
o'clock at night, and has not been heard of since. Was
dressed in,' etc., etc. Ha! That represents the last time that
the colonel needed to have his machine overhauled, I fancy."
"Good heavens!" cried my patient. "Then that explains what the
girl said."
"Undoubtedly. It is quite clear that the colonel was a cool and
desperate man, who was absolutely determined that nothing should
stand in the way of his little game, like those out-and-out
pirates who will leave no survivor from a captured ship. Well,
every moment now is precious, so if you feel equal to it we shall
go down to Scotland Yard at once as a preliminary to starting for
Eyford."
Some three hours or so afterwards we were all in the train
together, bound from Reading to the little Berkshire village.
There were Sherlock Holmes, the hydraulic engineer, Inspector
Bradstreet, of Scotland Yard, a plain-clothes man, and myself.
Bradstreet had spread an ordnance map of the county out upon the
seat and was busy with his compasses drawing a circle with Eyford
for its centre.
"There you are," said he. "That circle is drawn at a radius of
ten miles from the village. The place we want must be somewhere
near that line. You said ten miles, I think, sir."
"It was an hour's good drive."
"And you think that they brought you back all that way when you
were unconscious?"
"They must have done so. I have a confused memory, too, of having
been lifted and conveyed somewhere."
"What I cannot understand," said I, "is why they should have
spared you when they found you lying fainting in the garden.
Perhaps the villain was softened by the woman's entreaties."
"I hardly think that likely. I never saw a more inexorable face
in my life."
"Oh, we shall soon clear up all that," said Bradstreet. "Well, I
have drawn my circle, and I only wish I knew at what point upon
it the folk that we are in search of are to be found."
"I think I could lay my finger on it," said Holmes quietly.
"Really, now!" cried the inspector, "you have formed your
opinion! Come, now, we shall see who agrees with you. I say it is
south, for the country is more deserted there."
"And I say east," said my patient.
"I am for west," remarked the plain-clothes man. "There are
several quiet little villages up there."
"And I am for north," said I, "because there are no hills there,
and our friend says that he did not notice the carriage go up
any."
"Come," cried the inspector, laughing; "it's a very pretty
diversity of opinion. We have boxed the compass among us. Who do
you give your casting vote to?"
"You are all wrong."
"But we can't all be."
"Oh, yes, you can. This is my point." He placed his finger in the
centre of the circle. "This is where we shall find them."
"But the twelve-mile drive?" gasped Hatherley.
"Six out and six back. Nothing simpler. You say yourself that the
horse was fresh and glossy when you got in. How could it be that
if it had gone twelve miles over heavy roads?"
"Indeed, it is a likely ruse enough," observed Bradstreet
thoughtfully. "Of course there can be no doubt as to the nature
of this gang."
"None at all," said Holmes. "They are coiners on a large scale,
and have used the machine to form the amalgam which has taken the
place of silver."
"We have known for some time that a clever gang was at work,"
said the inspector. "They have been turning out half-crowns by
the thousand. We even traced them as far as Reading, but could
get no farther, for they had covered their traces in a way that
showed that they were very old hands. But now, thanks to this
lucky chance, I think that we have got them right enough."
But the inspector was mistaken, for those criminals were not
destined to fall into the hands of justice. As we rolled into
Eyford Station we saw a gigantic column of smoke which streamed
up from behind a small clump of trees in the neighbourhood and
hung like an immense ostrich feather over the landscape.
"A house on fire?" asked Bradstreet as the train steamed off
again on its way.
"Yes, sir!" said the station-master.
"When did it break out?"
"I hear that it was during the night, sir, but it has got worse,
and the whole place is in a blaze."
"Whose house is it?"
"Dr. Becher's."
"Tell me," broke in the engineer, "is Dr. Becher a German, very
thin, with a long, sharp nose?"
The station-master laughed heartily. "No, sir, Dr. Becher is an
Englishman, and there isn't a man in the parish who has a
better-lined waistcoat. But he has a gentleman staying with him,
a patient, as I understand, who is a foreigner, and he looks as
if a little good Berkshire beef would do him no harm."
The station-master had not finished his speech before we were all
hastening in the direction of the fire. The road topped a low
hill, and there was a great widespread whitewashed building in
front of us, spouting fire at every chink and window, while in
the garden in front three fire-engines were vainly striving to
keep the flames under.
"That's it!" cried Hatherley, in intense excitement. "There is
the gravel-drive, and there are the rose-bushes where I lay. That
second window is the one that I jumped from."
"Well, at least," said Holmes, "you have had your revenge upon
them. There can be no question that it was your oil-lamp which,
when it was crushed in the press, set fire to the wooden walls,
though no doubt they were too excited in the chase after you to
observe it at the time. Now keep your eyes open in this crowd for
your friends of last night, though I very much fear that they are
a good hundred miles off by now."
And Holmes' fears came to be realised, for from that day to this
no word has ever been heard either of the beautiful woman, the
sinister German, or the morose Englishman. Early that morning a
peasant had met a cart containing several people and some very
bulky boxes driving rapidly in the direction of Reading, but
there all traces of the fugitives disappeared, and even Holmes'
ingenuity failed ever to discover the least clue as to their
whereabouts.
The firemen had been much perturbed at the strange arrangements
which they had found within, and still more so by discovering a
newly severed human thumb upon a window-sill of the second floor.
About sunset, however, their efforts were at last successful, and
they subdued the flames, but not before the roof had fallen in,
and the whole place been reduced to such absolute ruin that, save
some twisted cylinders and iron piping, not a trace remained of
the machinery which had cost our unfortunate acquaintance so
dearly. Large masses of nickel and of tin were discovered stored
in an out-house, but no coins were to be found, which may have
explained the presence of those bulky boxes which have been
already referred to.
How our hydraulic engineer had been conveyed from the garden to
the spot where he recovered his senses might have remained
forever a mystery were it not for the soft mould, which told us a
very plain tale. He had evidently been carried down by two
persons, one of whom had remarkably small feet and the other
unusually large ones. On the whole, it was most probable that the
silent Englishman, being less bold or less murderous than his
companion, had assisted the woman to bear the unconscious man out
of the way of danger.
"Well," said our engineer ruefully as we took our seats to return
once more to London, "it has been a pretty business for me! I
have lost my thumb and I have lost a fifty-guinea fee, and what
have I gained?"
"Experience," said Holmes, laughing. "Indirectly it may be of
value, you know; you have only to put it into words to gain the
reputation of being excellent company for the remainder of your
existence."
X. THE ADVENTURE OF THE NOBLE BACHELOR
The Lord St. Simon marriage, and its curious termination, have
long ceased to be a subject of interest in those exalted circles
in which the unfortunate bridegroom moves. Fresh scandals have
eclipsed it, and their more piquant details have drawn the
gossips away from this four-year-old drama. As I have reason to
believe, however, that the full facts have never been revealed to
the general public, and as my friend Sherlock Holmes had a
considerable share in clearing the matter up, I feel that no
memoir of him would be complete without some little sketch of
this remarkable episode.
It was a few weeks before my own marriage, during the days when I
was still sharing rooms with Holmes in Baker Street, that he came
home from an afternoon stroll to find a letter on the table
waiting for him. I had remained indoors all day, for the weather
had taken a sudden turn to rain, with high autumnal winds, and
the Jezail bullet which I had brought back in one of my limbs as
a relic of my Afghan campaign throbbed with dull persistence.
With my body in one easy-chair and my legs upon another, I had
surrounded myself with a cloud of newspapers until at last,
saturated with the news of the day, I tossed them all aside and
lay listless, watching the huge crest and monogram upon the
envelope upon the table and wondering lazily who my friend's
noble correspondent could be.
"Here is a very fashionable epistle," I remarked as he entered.
"Your morning letters, if I remember right, were from a
fish-monger and a tide-waiter."
"Yes, my correspondence has certainly the charm of variety," he
answered, smiling, "and the humbler are usually the more
interesting. This looks like one of those unwelcome social
summonses which call upon a man either to be bored or to lie."
He broke the seal and glanced over the contents.
"Oh, come, it may prove to be something of interest, after all."
"Not social, then?"
"No, distinctly professional."
"And from a noble client?"
"One of the highest in England."
"My dear fellow, I congratulate you."
"I assure you, Watson, without affectation, that the status of my
client is a matter of less moment to me than the interest of his
case. It is just possible, however, that that also may not be
wanting in this new investigation. You have been reading the
papers diligently of late, have you not?"
"It looks like it," said I ruefully, pointing to a huge bundle in
the corner. "I have had nothing else to do."
"It is fortunate, for you will perhaps be able to post me up. I
read nothing except the criminal news and the agony column. The
latter is always instructive. But if you have followed recent
events so closely you must have read about Lord St. Simon and his
wedding?"
"Oh, yes, with the deepest interest."
"That is well. The letter which I hold in my hand is from Lord
St. Simon. I will read it to you, and in return you must turn
over these papers and let me have whatever bears upon the matter.
This is what he says:
"'MY DEAR MR. SHERLOCK HOLMES:--Lord Backwater tells me that I
may place implicit reliance upon your judgment and discretion. I
have determined, therefore, to call upon you and to consult you
in reference to the very painful event which has occurred in
connection with my wedding. Mr. Lestrade, of Scotland Yard, is
acting already in the matter, but he assures me that he sees no
objection to your co-operation, and that he even thinks that
it might be of some assistance. I will call at four o'clock in
the afternoon, and, should you have any other engagement at that
time, I hope that you will postpone it, as this matter is of
paramount importance. Yours faithfully, ST. SIMON.'
"It is dated from Grosvenor Mansions, written with a quill pen,
and the noble lord has had the misfortune to get a smear of ink
upon the outer side of his right little finger," remarked Holmes
as he folded up the epistle.
"He says four o'clock. It is three now. He will be here in an
hour."
"Then I have just time, with your assistance, to get clear upon
the subject. Turn over those papers and arrange the extracts in
their order of time, while I take a glance as to who our client
is." He picked a red-covered volume from a line of books of
reference beside the mantelpiece. "Here he is," said he, sitting
down and flattening it out upon his knee. "'Lord Robert Walsingham
de Vere St. Simon, second son of the Duke of Balmoral.' Hum! 'Arms:
Azure, three caltrops in chief over a fess sable. Born in 1846.'
He's forty-one years of age, which is mature for marriage. Was
Under-Secretary for the colonies in a late administration. The
Duke, his father, was at one time Secretary for Foreign Affairs.
They inherit Plantagenet blood by direct descent, and Tudor on
the distaff side. Ha! Well, there is nothing very instructive in
all this. I think that I must turn to you Watson, for something
more solid."
"I have very little difficulty in finding what I want," said I,
"for the facts are quite recent, and the matter struck me as
remarkable. I feared to refer them to you, however, as I knew
that you had an inquiry on hand and that you disliked the
intrusion of other matters."
"Oh, you mean the little problem of the Grosvenor Square
furniture van. That is quite cleared up now--though, indeed, it
was obvious from the first. Pray give me the results of your
newspaper selections."
"Here is the first notice which I can find. It is in the personal
column of the Morning Post, and dates, as you see, some weeks
back: 'A marriage has been arranged,' it says, 'and will, if
rumour is correct, very shortly take place, between Lord Robert
St. Simon, second son of the Duke of Balmoral, and Miss Hatty
Doran, the only daughter of Aloysius Doran. Esq., of San
Francisco, Cal., U.S.A.' That is all."
"Terse and to the point," remarked Holmes, stretching his long,
thin legs towards the fire.
"There was a paragraph amplifying this in one of the society
papers of the same week. Ah, here it is: 'There will soon be a
call for protection in the marriage market, for the present
free-trade principle appears to tell heavily against our home
product. One by one the management of the noble houses of Great
Britain is passing into the hands of our fair cousins from across
the Atlantic. An important addition has been made during the last
week to the list of the prizes which have been borne away by
these charming invaders. Lord St. Simon, who has shown himself
for over twenty years proof against the little god's arrows, has
now definitely announced his approaching marriage with Miss Hatty
Doran, the fascinating daughter of a California millionaire. Miss
Doran, whose graceful figure and striking face attracted much
attention at the Westbury House festivities, is an only child,
and it is currently reported that her dowry will run to
considerably over the six figures, with expectancies for the
future. As it is an open secret that the Duke of Balmoral has
been compelled to sell his pictures within the last few years,
and as Lord St. Simon has no property of his own save the small
estate of Birchmoor, it is obvious that the Californian heiress
is not the only gainer by an alliance which will enable her to
make the easy and common transition from a Republican lady to a
British peeress.'"
"Anything else?" asked Holmes, yawning.
"Oh, yes; plenty. Then there is another note in the Morning Post
to say that the marriage would be an absolutely quiet one, that it
would be at St. George's, Hanover Square, that only half a dozen
intimate friends would be invited, and that the party would
return to the furnished house at Lancaster Gate which has been
taken by Mr. Aloysius Doran. Two days later--that is, on
Wednesday last--there is a curt announcement that the wedding had
taken place, and that the honeymoon would be passed at Lord
Backwater's place, near Petersfield. Those are all the notices
which appeared before the disappearance of the bride."
"Before the what?" asked Holmes with a start.
"The vanishing of the lady."
"When did she vanish, then?"
"At the wedding breakfast."
"Indeed. This is more interesting than it promised to be; quite
dramatic, in fact."
"Yes; it struck me as being a little out of the common."
"They often vanish before the ceremony, and occasionally during
the honeymoon; but I cannot call to mind anything quite so prompt
as this. Pray let me have the details."
"I warn you that they are very incomplete."
"Perhaps we may make them less so."
"Such as they are, they are set forth in a single article of a
morning paper of yesterday, which I will read to you. It is
headed, 'Singular Occurrence at a Fashionable Wedding':
"'The family of Lord Robert St. Simon has been thrown into the
greatest consternation by the strange and painful episodes which
have taken place in connection with his wedding. The ceremony, as
shortly announced in the papers of yesterday, occurred on the
previous morning; but it is only now that it has been possible to
confirm the strange rumours which have been so persistently
floating about. In spite of the attempts of the friends to hush
the matter up, so much public attention has now been drawn to it
that no good purpose can be served by affecting to disregard what
is a common subject for conversation.
"'The ceremony, which was performed at St. George's, Hanover
Square, was a very quiet one, no one being present save the
father of the bride, Mr. Aloysius Doran, the Duchess of Balmoral,
Lord Backwater, Lord Eustace and Lady Clara St. Simon (the
younger brother and sister of the bridegroom), and Lady Alicia
Whittington. The whole party proceeded afterwards to the house of
Mr. Aloysius Doran, at Lancaster Gate, where breakfast had been
prepared. It appears that some little trouble was caused by a
woman, whose name has not been ascertained, who endeavoured to
force her way into the house after the bridal party, alleging
that she had some claim upon Lord St. Simon. It was only after a
painful and prolonged scene that she was ejected by the butler
and the footman. The bride, who had fortunately entered the house
before this unpleasant interruption, had sat down to breakfast
with the rest, when she complained of a sudden indisposition and
retired to her room. Her prolonged absence having caused some
comment, her father followed her, but learned from her maid that
she had only come up to her chamber for an instant, caught up an
ulster and bonnet, and hurried down to the passage. One of the
footmen declared that he had seen a lady leave the house thus
apparelled, but had refused to credit that it was his mistress,
believing her to be with the company. On ascertaining that his
daughter had disappeared, Mr. Aloysius Doran, in conjunction with
the bridegroom, instantly put themselves in communication with
the police, and very energetic inquiries are being made, which
will probably result in a speedy clearing up of this very
singular business. Up to a late hour last night, however, nothing
had transpired as to the whereabouts of the missing lady. There
are rumours of foul play in the matter, and it is said that the
police have caused the arrest of the woman who had caused the
original disturbance, in the belief that, from jealousy or some
other motive, she may have been concerned in the strange
disappearance of the bride.'"
"And is that all?"
"Only one little item in another of the morning papers, but it is
a suggestive one."
"And it is--"
"That Miss Flora Millar, the lady who had caused the disturbance,
has actually been arrested. It appears that she was formerly a
danseuse at the Allegro, and that she has known the bridegroom
for some years. There are no further particulars, and the whole
case is in your hands now--so far as it has been set forth in the
public press."
"And an exceedingly interesting case it appears to be. I would
not have missed it for worlds. But there is a ring at the bell,
Watson, and as the clock makes it a few minutes after four, I
have no doubt that this will prove to be our noble client. Do not
dream of going, Watson, for I very much prefer having a witness,
if only as a check to my own memory."
"Lord Robert St. Simon," announced our page-boy, throwing open
the door. A gentleman entered, with a pleasant, cultured face,
high-nosed and pale, with something perhaps of petulance about
the mouth, and with the steady, well-opened eye of a man whose
pleasant lot it had ever been to command and to be obeyed. His
manner was brisk, and yet his general appearance gave an undue
impression of age, for he had a slight forward stoop and a little
bend of the knees as he walked. His hair, too, as he swept off
his very curly-brimmed hat, was grizzled round the edges and thin
upon the top. As to his dress, it was careful to the verge of
foppishness, with high collar, black frock-coat, white waistcoat,
yellow gloves, patent-leather shoes, and light-coloured gaiters.
He advanced slowly into the room, turning his head from left to
right, and swinging in his right hand the cord which held his
golden eyeglasses.
"Good-day, Lord St. Simon," said Holmes, rising and bowing. "Pray
take the basket-chair. This is my friend and colleague, Dr.
Watson. Draw up a little to the fire, and we will talk this
matter over."
"A most painful matter to me, as you can most readily imagine,
Mr. Holmes. I have been cut to the quick. I understand that you
have already managed several delicate cases of this sort, sir,
though I presume that they were hardly from the same class of
society."
"No, I am descending."
"I beg pardon."
"My last client of the sort was a king."
"Oh, really! I had no idea. And which king?"
"The King of Scandinavia."
"What! Had he lost his wife?"
"You can understand," said Holmes suavely, "that I extend to the
affairs of my other clients the same secrecy which I promise to
you in yours."
"Of course! Very right! very right! I'm sure I beg pardon. As to
my own case, I am ready to give you any information which may
assist you in forming an opinion."
"Thank you. I have already learned all that is in the public
prints, nothing more. I presume that I may take it as correct--this
article, for example, as to the disappearance of the bride."
Lord St. Simon glanced over it. "Yes, it is correct, as far as it
goes."
"But it needs a great deal of supplementing before anyone could
offer an opinion. I think that I may arrive at my facts most
directly by questioning you."
"Pray do so."
"When did you first meet Miss Hatty Doran?"
"In San Francisco, a year ago."
"You were travelling in the States?"
"Yes."
"Did you become engaged then?"
"No."
"But you were on a friendly footing?"
"I was amused by her society, and she could see that I was
amused."
"Her father is very rich?"
"He is said to be the richest man on the Pacific slope."
"And how did he make his money?"
"In mining. He had nothing a few years ago. Then he struck gold,
invested it, and came up by leaps and bounds."
"Now, what is your own impression as to the young lady's--your
wife's character?"
The nobleman swung his glasses a little faster and stared down
into the fire. "You see, Mr. Holmes," said he, "my wife was
twenty before her father became a rich man. During that time she
ran free in a mining camp and wandered through woods or
mountains, so that her education has come from Nature rather than
from the schoolmaster. She is what we call in England a tomboy,
with a strong nature, wild and free, unfettered by any sort of
traditions. She is impetuous--volcanic, I was about to say. She
is swift in making up her mind and fearless in carrying out her
resolutions. On the other hand, I would not have given her the
name which I have the honour to bear"--he gave a little stately
cough--"had not I thought her to be at bottom a noble woman. I
believe that she is capable of heroic self-sacrifice and that
anything dishonourable would be repugnant to her."
"Have you her photograph?"
"I brought this with me." He opened a locket and showed us the
full face of a very lovely woman. It was not a photograph but an
ivory miniature, and the artist had brought out the full effect
of the lustrous black hair, the large dark eyes, and the
exquisite mouth. Holmes gazed long and earnestly at it. Then he
closed the locket and handed it back to Lord St. Simon.
"The young lady came to London, then, and you renewed your
acquaintance?"
"Yes, her father brought her over for this last London season. I
met her several times, became engaged to her, and have now
married her."
"She brought, I understand, a considerable dowry?"
"A fair dowry. Not more than is usual in my family."
"And this, of course, remains to you, since the marriage is a
fait accompli?"
"I really have made no inquiries on the subject."
"Very naturally not. Did you see Miss Doran on the day before the
wedding?"
"Yes."
"Was she in good spirits?"
"Never better. She kept talking of what we should do in our
future lives."
"Indeed! That is very interesting. And on the morning of the
wedding?"
"She was as bright as possible--at least until after the
ceremony."
"And did you observe any change in her then?"
"Well, to tell the truth, I saw then the first signs that I had
ever seen that her temper was just a little sharp. The incident
however, was too trivial to relate and can have no possible
bearing upon the case."
"Pray let us have it, for all that."
"Oh, it is childish. She dropped her bouquet as we went towards
the vestry. She was passing the front pew at the time, and it
fell over into the pew. There was a moment's delay, but the
gentleman in the pew handed it up to her again, and it did not
appear to be the worse for the fall. Yet when I spoke to her of
the matter, she answered me abruptly; and in the carriage, on our
way home, she seemed absurdly agitated over this trifling cause."
"Indeed! You say that there was a gentleman in the pew. Some of
the general public were present, then?"
"Oh, yes. It is impossible to exclude them when the church is
open."
"This gentleman was not one of your wife's friends?"
"No, no; I call him a gentleman by courtesy, but he was quite a
common-looking person. I hardly noticed his appearance. But
really I think that we are wandering rather far from the point."
"Lady St. Simon, then, returned from the wedding in a less
cheerful frame of mind than she had gone to it. What did she do
on re-entering her father's house?"
"I saw her in conversation with her maid."
"And who is her maid?"
"Alice is her name. She is an American and came from California
with her."
"A confidential servant?"
"A little too much so. It seemed to me that her mistress allowed
her to take great liberties. Still, of course, in America they
look upon these things in a different way."
"How long did she speak to this Alice?"
"Oh, a few minutes. I had something else to think of."
"You did not overhear what they said?"
"Lady St. Simon said something about 'jumping a claim.' She was
accustomed to use slang of the kind. I have no idea what she
meant."
"American slang is very expressive sometimes. And what did your
wife do when she finished speaking to her maid?"
"She walked into the breakfast-room."
"On your arm?"
"No, alone. She was very independent in little matters like that.
Then, after we had sat down for ten minutes or so, she rose
hurriedly, muttered some words of apology, and left the room. She
never came back."
"But this maid, Alice, as I understand, deposes that she went to
her room, covered her bride's dress with a long ulster, put on a
bonnet, and went out."
"Quite so. And she was afterwards seen walking into Hyde Park in
company with Flora Millar, a woman who is now in custody, and who
had already made a disturbance at Mr. Doran's house that
morning."
"Ah, yes. I should like a few particulars as to this young lady,
and your relations to her."
Lord St. Simon shrugged his shoulders and raised his eyebrows.
"We have been on a friendly footing for some years--I may say on
a very friendly footing. She used to be at the Allegro. I have
not treated her ungenerously, and she had no just cause of
complaint against me, but you know what women are, Mr. Holmes.
Flora was a dear little thing, but exceedingly hot-headed and
devotedly attached to me. She wrote me dreadful letters when she
heard that I was about to be married, and, to tell the truth, the
reason why I had the marriage celebrated so quietly was that I
feared lest there might be a scandal in the church. She came to
Mr. Doran's door just after we returned, and she endeavoured to
push her way in, uttering very abusive expressions towards my
wife, and even threatening her, but I had foreseen the
possibility of something of the sort, and I had two police
fellows there in private clothes, who soon pushed her out again.
She was quiet when she saw that there was no good in making a
row."
"Did your wife hear all this?"
"No, thank goodness, she did not."
"And she was seen walking with this very woman afterwards?"
"Yes. That is what Mr. Lestrade, of Scotland Yard, looks upon as
so serious. It is thought that Flora decoyed my wife out and laid
some terrible trap for her."
"Well, it is a possible supposition."
"You think so, too?"
"I did not say a probable one. But you do not yourself look upon
this as likely?"
"I do not think Flora would hurt a fly."
"Still, jealousy is a strange transformer of characters. Pray
what is your own theory as to what took place?"
"Well, really, I came to seek a theory, not to propound one. I
have given you all the facts. Since you ask me, however, I may
say that it has occurred to me as possible that the excitement of
this affair, the consciousness that she had made so immense a
social stride, had the effect of causing some little nervous
disturbance in my wife."
"In short, that she had become suddenly deranged?"
"Well, really, when I consider that she has turned her back--I
will not say upon me, but upon so much that many have aspired to
without success--I can hardly explain it in any other fashion."
"Well, certainly that is also a conceivable hypothesis," said
Holmes, smiling. "And now, Lord St. Simon, I think that I have
nearly all my data. May I ask whether you were seated at the
breakfast-table so that you could see out of the window?"
"We could see the other side of the road and the Park."
"Quite so. Then I do not think that I need to detain you longer.
I shall communicate with you."
"Should you be fortunate enough to solve this problem," said our
client, rising.
"I have solved it."
"Eh? What was that?"
"I say that I have solved it."
"Where, then, is my wife?"
"That is a detail which I shall speedily supply."
Lord St. Simon shook his head. "I am afraid that it will take
wiser heads than yours or mine," he remarked, and bowing in a
stately, old-fashioned manner he departed.
"It is very good of Lord St. Simon to honour my head by putting
it on a level with his own," said Sherlock Holmes, laughing. "I
think that I shall have a whisky and soda and a cigar after all
this cross-questioning. I had formed my conclusions as to the
case before our client came into the room."
"My dear Holmes!"
"I have notes of several similar cases, though none, as I
remarked before, which were quite as prompt. My whole examination
served to turn my conjecture into a certainty. Circumstantial
evidence is occasionally very convincing, as when you find a
trout in the milk, to quote Thoreau's example."
"But I have heard all that you have heard."
"Without, however, the knowledge of pre-existing cases which
serves me so well. There was a parallel instance in Aberdeen some
years back, and something on very much the same lines at Munich
the year after the Franco-Prussian War. It is one of these
cases--but, hullo, here is Lestrade! Good-afternoon, Lestrade!
You will find an extra tumbler upon the sideboard, and there are
cigars in the box."
The official detective was attired in a pea-jacket and cravat,
which gave him a decidedly nautical appearance, and he carried a
black canvas bag in his hand. With a short greeting he seated
himself and lit the cigar which had been offered to him.
"What's up, then?" asked Holmes with a twinkle in his eye. "You
look dissatisfied."
"And I feel dissatisfied. It is this infernal St. Simon marriage
case. I can make neither head nor tail of the business."
"Really! You surprise me."
"Who ever heard of such a mixed affair? Every clue seems to slip
through my fingers. I have been at work upon it all day."
"And very wet it seems to have made you," said Holmes laying his
hand upon the arm of the pea-jacket.
"Yes, I have been dragging the Serpentine."
"In heaven's name, what for?"
"In search of the body of Lady St. Simon."
Sherlock Holmes leaned back in his chair and laughed heartily.
"Have you dragged the basin of Trafalgar Square fountain?" he
asked.
"Why? What do you mean?"
"Because you have just as good a chance of finding this lady in
the one as in the other."
Lestrade shot an angry glance at my companion. "I suppose you
know all about it," he snarled.
"Well, I have only just heard the facts, but my mind is made up."
"Oh, indeed! Then you think that the Serpentine plays no part in
the matter?"
"I think it very unlikely."
"Then perhaps you will kindly explain how it is that we found
this in it?" He opened his bag as he spoke, and tumbled onto the
floor a wedding-dress of watered silk, a pair of white satin
shoes and a bride's wreath and veil, all discoloured and soaked
in water. "There," said he, putting a new wedding-ring upon the
top of the pile. "There is a little nut for you to crack, Master
Holmes."
"Oh, indeed!" said my friend, blowing blue rings into the air.
"You dragged them from the Serpentine?"
"No. They were found floating near the margin by a park-keeper.
They have been identified as her clothes, and it seemed to me
that if the clothes were there the body would not be far off."
"By the same brilliant reasoning, every man's body is to be found
in the neighbourhood of his wardrobe. And pray what did you hope
to arrive at through this?"
"At some evidence implicating Flora Millar in the disappearance."
"I am afraid that you will find it difficult."
"Are you, indeed, now?" cried Lestrade with some bitterness. "I
am afraid, Holmes, that you are not very practical with your
deductions and your inferences. You have made two blunders in as
many minutes. This dress does implicate Miss Flora Millar."
"And how?"
"In the dress is a pocket. In the pocket is a card-case. In the
card-case is a note. And here is the very note." He slapped it
down upon the table in front of him. "Listen to this: 'You will
see me when all is ready. Come at once. F.H.M.' Now my theory all
along has been that Lady St. Simon was decoyed away by Flora
Millar, and that she, with confederates, no doubt, was
responsible for her disappearance. Here, signed with her
initials, is the very note which was no doubt quietly slipped
into her hand at the door and which lured her within their
reach."
"Very good, Lestrade," said Holmes, laughing. "You really are
very fine indeed. Let me see it." He took up the paper in a
listless way, but his attention instantly became riveted, and he
gave a little cry of satisfaction. "This is indeed important,"
said he.
"Ha! you find it so?"
"Extremely so. I congratulate you warmly."
Lestrade rose in his triumph and bent his head to look. "Why," he
shrieked, "you're looking at the wrong side!"
"On the contrary, this is the right side."
"The right side? You're mad! Here is the note written in pencil
over here."
"And over here is what appears to be the fragment of a hotel
bill, which interests me deeply."
"There's nothing in it. I looked at it before," said Lestrade.
"'Oct. 4th, rooms 8s., breakfast 2s. 6d., cocktail 1s., lunch 2s.
6d., glass sherry, 8d.' I see nothing in that."
"Very likely not. It is most important, all the same. As to the
note, it is important also, or at least the initials are, so I
congratulate you again."
"I've wasted time enough," said Lestrade, rising. "I believe in
hard work and not in sitting by the fire spinning fine theories.
Good-day, Mr. Holmes, and we shall see which gets to the bottom
of the matter first." He gathered up the garments, thrust them
into the bag, and made for the door.
"Just one hint to you, Lestrade," drawled Holmes before his rival
vanished; "I will tell you the true solution of the matter. Lady
St. Simon is a myth. There is not, and there never has been, any
such person."
Lestrade looked sadly at my companion. Then he turned to me,
tapped his forehead three times, shook his head solemnly, and
hurried away.
He had hardly shut the door behind him when Holmes rose to put on
his overcoat. "There is something in what the fellow says about
outdoor work," he remarked, "so I think, Watson, that I must
leave you to your papers for a little."
It was after five o'clock when Sherlock Holmes left me, but I had
no time to be lonely, for within an hour there arrived a
confectioner's man with a very large flat box. This he unpacked
with the help of a youth whom he had brought with him, and
presently, to my very great astonishment, a quite epicurean
little cold supper began to be laid out upon our humble
lodging-house mahogany. There were a couple of brace of cold
woodcock, a pheasant, a pâté de foie gras pie with a group of
ancient and cobwebby bottles. Having laid out all these luxuries,
my two visitors vanished away, like the genii of the Arabian
Nights, with no explanation save that the things had been paid
for and were ordered to this address.
Just before nine o'clock Sherlock Holmes stepped briskly into the
room. His features were gravely set, but there was a light in his
eye which made me think that he had not been disappointed in his
conclusions.
"They have laid the supper, then," he said, rubbing his hands.
"You seem to expect company. They have laid for five."
"Yes, I fancy we may have some company dropping in," said he. "I
am surprised that Lord St. Simon has not already arrived. Ha! I
fancy that I hear his step now upon the stairs."
It was indeed our visitor of the afternoon who came bustling in,
dangling his glasses more vigorously than ever, and with a very
perturbed expression upon his aristocratic features.
"My messenger reached you, then?" asked Holmes.
"Yes, and I confess that the contents startled me beyond measure.
Have you good authority for what you say?"
"The best possible."
Lord St. Simon sank into a chair and passed his hand over his
forehead.
"What will the Duke say," he murmured, "when he hears that one of
the family has been subjected to such humiliation?"
"It is the purest accident. I cannot allow that there is any
humiliation."
"Ah, you look on these things from another standpoint."
"I fail to see that anyone is to blame. I can hardly see how the
lady could have acted otherwise, though her abrupt method of
doing it was undoubtedly to be regretted. Having no mother, she
had no one to advise her at such a crisis."
"It was a slight, sir, a public slight," said Lord St. Simon,
tapping his fingers upon the table.
"You must make allowance for this poor girl, placed in so
unprecedented a position."
"I will make no allowance. I am very angry indeed, and I have
been shamefully used."
"I think that I heard a ring," said Holmes. "Yes, there are steps
on the landing. If I cannot persuade you to take a lenient view
of the matter, Lord St. Simon, I have brought an advocate here
who may be more successful." He opened the door and ushered in a
lady and gentleman. "Lord St. Simon," said he "allow me to
introduce you to Mr. and Mrs. Francis Hay Moulton. The lady, I
think, you have already met."
At the sight of these newcomers our client had sprung from his
seat and stood very erect, with his eyes cast down and his hand
thrust into the breast of his frock-coat, a picture of offended
dignity. The lady had taken a quick step forward and had held out
her hand to him, but he still refused to raise his eyes. It was
as well for his resolution, perhaps, for her pleading face was
one which it was hard to resist.
"You're angry, Robert," said she. "Well, I guess you have every
cause to be."
"Pray make no apology to me," said Lord St. Simon bitterly.
"Oh, yes, I know that I have treated you real bad and that I
should have spoken to you before I went; but I was kind of
rattled, and from the time when I saw Frank here again I just
didn't know what I was doing or saying. I only wonder I didn't
fall down and do a faint right there before the altar."
"Perhaps, Mrs. Moulton, you would like my friend and me to leave
the room while you explain this matter?"
"If I may give an opinion," remarked the strange gentleman,
"we've had just a little too much secrecy over this business
already. For my part, I should like all Europe and America to
hear the rights of it." He was a small, wiry, sunburnt man,
clean-shaven, with a sharp face and alert manner.
"Then I'll tell our story right away," said the lady. "Frank here
and I met in '84, in McQuire's camp, near the Rockies, where pa
was working a claim. We were engaged to each other, Frank and I;
but then one day father struck a rich pocket and made a pile,
while poor Frank here had a claim that petered out and came to
nothing. The richer pa grew the poorer was Frank; so at last pa
wouldn't hear of our engagement lasting any longer, and he took
me away to 'Frisco. Frank wouldn't throw up his hand, though; so
he followed me there, and he saw me without pa knowing anything
about it. It would only have made him mad to know, so we just
fixed it all up for ourselves. Frank said that he would go and
make his pile, too, and never come back to claim me until he had
as much as pa. So then I promised to wait for him to the end of
time and pledged myself not to marry anyone else while he lived.
'Why shouldn't we be married right away, then,' said he, 'and
then I will feel sure of you; and I won't claim to be your
husband until I come back?' Well, we talked it over, and he had
fixed it all up so nicely, with a clergyman all ready in waiting,
that we just did it right there; and then Frank went off to seek
his fortune, and I went back to pa.
"The next I heard of Frank was that he was in Montana, and then
he went prospecting in Arizona, and then I heard of him from New
Mexico. After that came a long newspaper story about how a
miners' camp had been attacked by Apache Indians, and there was
my Frank's name among the killed. I fainted dead away, and I was
very sick for months after. Pa thought I had a decline and took
me to half the doctors in 'Frisco. Not a word of news came for a
year and more, so that I never doubted that Frank was really
dead. Then Lord St. Simon came to 'Frisco, and we came to London,
and a marriage was arranged, and pa was very pleased, but I felt
all the time that no man on this earth would ever take the place
in my heart that had been given to my poor Frank.
"Still, if I had married Lord St. Simon, of course I'd have done
my duty by him. We can't command our love, but we can our
actions. I went to the altar with him with the intention to make
him just as good a wife as it was in me to be. But you may
imagine what I felt when, just as I came to the altar rails, I
glanced back and saw Frank standing and looking at me out of the
first pew. I thought it was his ghost at first; but when I looked
again there he was still, with a kind of question in his eyes, as
if to ask me whether I were glad or sorry to see him. I wonder I
didn't drop. I know that everything was turning round, and the
words of the clergyman were just like the buzz of a bee in my
ear. I didn't know what to do. Should I stop the service and make
a scene in the church? I glanced at him again, and he seemed to
know what I was thinking, for he raised his finger to his lips to
tell me to be still. Then I saw him scribble on a piece of paper,
and I knew that he was writing me a note. As I passed his pew on
the way out I dropped my bouquet over to him, and he slipped the
note into my hand when he returned me the flowers. It was only a
line asking me to join him when he made the sign to me to do so.
Of course I never doubted for a moment that my first duty was now
to him, and I determined to do just whatever he might direct.
"When I got back I told my maid, who had known him in California,
and had always been his friend. I ordered her to say nothing, but
to get a few things packed and my ulster ready. I know I ought to
have spoken to Lord St. Simon, but it was dreadful hard before
his mother and all those great people. I just made up my mind to
run away and explain afterwards. I hadn't been at the table ten
minutes before I saw Frank out of the window at the other side of
the road. He beckoned to me and then began walking into the Park.
I slipped out, put on my things, and followed him. Some woman
came talking something or other about Lord St. Simon to
me--seemed to me from the little I heard as if he had a little
secret of his own before marriage also--but I managed to get away
from her and soon overtook Frank. We got into a cab together, and
away we drove to some lodgings he had taken in Gordon Square, and
that was my true wedding after all those years of waiting. Frank
had been a prisoner among the Apaches, had escaped, came on to
'Frisco, found that I had given him up for dead and had gone to
England, followed me there, and had come upon me at last on the
very morning of my second wedding."
"I saw it in a paper," explained the American. "It gave the name
and the church but not where the lady lived."
"Then we had a talk as to what we should do, and Frank was all
for openness, but I was so ashamed of it all that I felt as if I
should like to vanish away and never see any of them again--just
sending a line to pa, perhaps, to show him that I was alive. It
was awful to me to think of all those lords and ladies sitting
round that breakfast-table and waiting for me to come back. So
Frank took my wedding-clothes and things and made a bundle of
them, so that I should not be traced, and dropped them away
somewhere where no one could find them. It is likely that we
should have gone on to Paris to-morrow, only that this good
gentleman, Mr. Holmes, came round to us this evening, though how
he found us is more than I can think, and he showed us very
clearly and kindly that I was wrong and that Frank was right, and
that we should be putting ourselves in the wrong if we were so
secret. Then he offered to give us a chance of talking to Lord
St. Simon alone, and so we came right away round to his rooms at
once. Now, Robert, you have heard it all, and I am very sorry if
I have given you pain, and I hope that you do not think very
meanly of me."
Lord St. Simon had by no means relaxed his rigid attitude, but
had listened with a frowning brow and a compressed lip to this
long narrative.
"Excuse me," he said, "but it is not my custom to discuss my most
intimate personal affairs in this public manner."
"Then you won't forgive me? You won't shake hands before I go?"
"Oh, certainly, if it would give you any pleasure." He put out
his hand and coldly grasped that which she extended to him.
"I had hoped," suggested Holmes, "that you would have joined us
in a friendly supper."
"I think that there you ask a little too much," responded his
Lordship. "I may be forced to acquiesce in these recent
developments, but I can hardly be expected to make merry over
them. I think that with your permission I will now wish you all a
very good-night." He included us all in a sweeping bow and
stalked out of the room.
"Then I trust that you at least will honour me with your
company," said Sherlock Holmes. "It is always a joy to meet an
American, Mr. Moulton, for I am one of those who believe that the
folly of a monarch and the blundering of a minister in far-gone
years will not prevent our children from being some day citizens
of the same world-wide country under a flag which shall be a
quartering of the Union Jack with the Stars and Stripes."
"The case has been an interesting one," remarked Holmes when our
visitors had left us, "because it serves to show very clearly how
simple the explanation may be of an affair which at first sight
seems to be almost inexplicable. Nothing could be more natural
than the sequence of events as narrated by this lady, and nothing
stranger than the result when viewed, for instance, by Mr.
Lestrade of Scotland Yard."
"You were not yourself at fault at all, then?"
"From the first, two facts were very obvious to me, the one that
the lady had been quite willing to undergo the wedding ceremony,
the other that she had repented of it within a few minutes of
returning home. Obviously something had occurred during the
morning, then, to cause her to change her mind. What could that
something be? She could not have spoken to anyone when she was
out, for she had been in the company of the bridegroom. Had she
seen someone, then? If she had, it must be someone from America
because she had spent so short a time in this country that she
could hardly have allowed anyone to acquire so deep an influence
over her that the mere sight of him would induce her to change
her plans so completely. You see we have already arrived, by a
process of exclusion, at the idea that she might have seen an
American. Then who could this American be, and why should he
possess so much influence over her? It might be a lover; it might
be a husband. Her young womanhood had, I knew, been spent in
rough scenes and under strange conditions. So far I had got
before I ever heard Lord St. Simon's narrative. When he told us
of a man in a pew, of the change in the bride's manner, of so
transparent a device for obtaining a note as the dropping of a
bouquet, of her resort to her confidential maid, and of her very
significant allusion to claim-jumping--which in miners' parlance
means taking possession of that which another person has a prior
claim to--the whole situation became absolutely clear. She had
gone off with a man, and the man was either a lover or was a
previous husband--the chances being in favour of the latter."
"And how in the world did you find them?"
"It might have been difficult, but friend Lestrade held
information in his hands the value of which he did not himself
know. The initials were, of course, of the highest importance,
but more valuable still was it to know that within a week he had
settled his bill at one of the most select London hotels."
"How did you deduce the select?"
"By the select prices. Eight shillings for a bed and eightpence
for a glass of sherry pointed to one of the most expensive
hotels. There are not many in London which charge at that rate.
In the second one which I visited in Northumberland Avenue, I
learned by an inspection of the book that Francis H. Moulton, an
American gentleman, had left only the day before, and on looking
over the entries against him, I came upon the very items which I
had seen in the duplicate bill. His letters were to be forwarded
to 226 Gordon Square; so thither I travelled, and being fortunate
enough to find the loving couple at home, I ventured to give them
some paternal advice and to point out to them that it would be
better in every way that they should make their position a little
clearer both to the general public and to Lord St. Simon in
particular. I invited them to meet him here, and, as you see, I
made him keep the appointment."
"But with no very good result," I remarked. "His conduct was
certainly not very gracious."
"Ah, Watson," said Holmes, smiling, "perhaps you would not be
very gracious either, if, after all the trouble of wooing and
wedding, you found yourself deprived in an instant of wife and of
fortune. I think that we may judge Lord St. Simon very mercifully
and thank our stars that we are never likely to find ourselves in
the same position. Draw your chair up and hand me my violin, for
the only problem we have still to solve is how to while away
these bleak autumnal evenings."
XI. THE ADVENTURE OF THE BERYL CORONET
"Holmes," said I as I stood one morning in our bow-window looking
down the street, "here is a madman coming along. It seems rather
sad that his relatives should allow him to come out alone."
My friend rose lazily from his armchair and stood with his hands
in the pockets of his dressing-gown, looking over my shoulder. It
was a bright, crisp February morning, and the snow of the day
before still lay deep upon the ground, shimmering brightly in the
wintry sun. Down the centre of Baker Street it had been ploughed
into a brown crumbly band by the traffic, but at either side and
on the heaped-up edges of the foot-paths it still lay as white as
when it fell. The grey pavement had been cleaned and scraped, but
was still dangerously slippery, so that there were fewer
passengers than usual. Indeed, from the direction of the
Metropolitan Station no one was coming save the single gentleman
whose eccentric conduct had drawn my attention.
He was a man of about fifty, tall, portly, and imposing, with a
massive, strongly marked face and a commanding figure. He was
dressed in a sombre yet rich style, in black frock-coat, shining
hat, neat brown gaiters, and well-cut pearl-grey trousers. Yet
his actions were in absurd contrast to the dignity of his dress
and features, for he was running hard, with occasional little
springs, such as a weary man gives who is little accustomed to
set any tax upon his legs. As he ran he jerked his hands up and
down, waggled his head, and writhed his face into the most
extraordinary contortions.
"What on earth can be the matter with him?" I asked. "He is
looking up at the numbers of the houses."
"I believe that he is coming here," said Holmes, rubbing his
hands.
"Here?"
"Yes; I rather think he is coming to consult me professionally. I
think that I recognise the symptoms. Ha! did I not tell you?" As
he spoke, the man, puffing and blowing, rushed at our door and
pulled at our bell until the whole house resounded with the
clanging.
A few moments later he was in our room, still puffing, still
gesticulating, but with so fixed a look of grief and despair in
his eyes that our smiles were turned in an instant to horror and
pity. For a while he could not get his words out, but swayed his
body and plucked at his hair like one who has been driven to the
extreme limits of his reason. Then, suddenly springing to his
feet, he beat his head against the wall with such force that we
both rushed upon him and tore him away to the centre of the room.
Sherlock Holmes pushed him down into the easy-chair and, sitting
beside him, patted his hand and chatted with him in the easy,
soothing tones which he knew so well how to employ.
"You have come to me to tell your story, have you not?" said he.
"You are fatigued with your haste. Pray wait until you have
recovered yourself, and then I shall be most happy to look into
any little problem which you may submit to me."
The man sat for a minute or more with a heaving chest, fighting
against his emotion. Then he passed his handkerchief over his
brow, set his lips tight, and turned his face towards us.
"No doubt you think me mad?" said he.
"I see that you have had some great trouble," responded Holmes.
"God knows I have!--a trouble which is enough to unseat my
reason, so sudden and so terrible is it. Public disgrace I might
have faced, although I am a man whose character has never yet
borne a stain. Private affliction also is the lot of every man;
but the two coming together, and in so frightful a form, have
been enough to shake my very soul. Besides, it is not I alone.
The very noblest in the land may suffer unless some way be found
out of this horrible affair."
"Pray compose yourself, sir," said Holmes, "and let me have a
clear account of who you are and what it is that has befallen
you."
"My name," answered our visitor, "is probably familiar to your
ears. I am Alexander Holder, of the banking firm of Holder &
Stevenson, of Threadneedle Street."
The name was indeed well known to us as belonging to the senior
partner in the second largest private banking concern in the City
of London. What could have happened, then, to bring one of the
foremost citizens of London to this most pitiable pass? We
waited, all curiosity, until with another effort he braced
himself to tell his story.
"I feel that time is of value," said he; "that is why I hastened
here when the police inspector suggested that I should secure
your co-operation. I came to Baker Street by the Underground and
hurried from there on foot, for the cabs go slowly through this
snow. That is why I was so out of breath, for I am a man who
takes very little exercise. I feel better now, and I will put the
facts before you as shortly and yet as clearly as I can.
"It is, of course, well known to you that in a successful banking
business as much depends upon our being able to find remunerative
investments for our funds as upon our increasing our connection
and the number of our depositors. One of our most lucrative means
of laying out money is in the shape of loans, where the security
is unimpeachable. We have done a good deal in this direction
during the last few years, and there are many noble families to
whom we have advanced large sums upon the security of their
pictures, libraries, or plate.
"Yesterday morning I was seated in my office at the bank when a
card was brought in to me by one of the clerks. I started when I
saw the name, for it was that of none other than--well, perhaps
even to you I had better say no more than that it was a name
which is a household word all over the earth--one of the highest,
noblest, most exalted names in England. I was overwhelmed by the
honour and attempted, when he entered, to say so, but he plunged
at once into business with the air of a man who wishes to hurry
quickly through a disagreeable task.
"'Mr. Holder,' said he, 'I have been informed that you are in the
habit of advancing money.'
"'The firm does so when the security is good.' I answered.
"'It is absolutely essential to me,' said he, 'that I should have
50,000 pounds at once. I could, of course, borrow so trifling a
sum ten times over from my friends, but I much prefer to make it
a matter of business and to carry out that business myself. In my
position you can readily understand that it is unwise to place
one's self under obligations.'
"'For how long, may I ask, do you want this sum?' I asked.
"'Next Monday I have a large sum due to me, and I shall then most
certainly repay what you advance, with whatever interest you
think it right to charge. But it is very essential to me that the
money should be paid at once.'
"'I should be happy to advance it without further parley from my
own private purse,' said I, 'were it not that the strain would be
rather more than it could bear. If, on the other hand, I am to do
it in the name of the firm, then in justice to my partner I must
insist that, even in your case, every businesslike precaution
should be taken.'
"'I should much prefer to have it so,' said he, raising up a
square, black morocco case which he had laid beside his chair.
'You have doubtless heard of the Beryl Coronet?'
"'One of the most precious public possessions of the empire,'
said I.
"'Precisely.' He opened the case, and there, imbedded in soft,
flesh-coloured velvet, lay the magnificent piece of jewellery
which he had named. 'There are thirty-nine enormous beryls,' said
he, 'and the price of the gold chasing is incalculable. The
lowest estimate would put the worth of the coronet at double the
sum which I have asked. I am prepared to leave it with you as my
security.'
"I took the precious case into my hands and looked in some
perplexity from it to my illustrious client.
"'You doubt its value?' he asked.
"'Not at all. I only doubt--'
"'The propriety of my leaving it. You may set your mind at rest
about that. I should not dream of doing so were it not absolutely
certain that I should be able in four days to reclaim it. It is a
pure matter of form. Is the security sufficient?'
"'Ample.'
"'You understand, Mr. Holder, that I am giving you a strong proof
of the confidence which I have in you, founded upon all that I
have heard of you. I rely upon you not only to be discreet and to
refrain from all gossip upon the matter but, above all, to
preserve this coronet with every possible precaution because I
need not say that a great public scandal would be caused if any
harm were to befall it. Any injury to it would be almost as
serious as its complete loss, for there are no beryls in the
world to match these, and it would be impossible to replace them.
I leave it with you, however, with every confidence, and I shall
call for it in person on Monday morning.'
"Seeing that my client was anxious to leave, I said no more but,
calling for my cashier, I ordered him to pay over fifty 1000
pound notes. When I was alone once more, however, with the
precious case lying upon the table in front of me, I could not
but think with some misgivings of the immense responsibility
which it entailed upon me. There could be no doubt that, as it
was a national possession, a horrible scandal would ensue if any
misfortune should occur to it. I already regretted having ever
consented to take charge of it. However, it was too late to alter
the matter now, so I locked it up in my private safe and turned
once more to my work.
"When evening came I felt that it would be an imprudence to leave
so precious a thing in the office behind me. Bankers' safes had
been forced before now, and why should not mine be? If so, how
terrible would be the position in which I should find myself! I
determined, therefore, that for the next few days I would always
carry the case backward and forward with me, so that it might
never be really out of my reach. With this intention, I called a
cab and drove out to my house at Streatham, carrying the jewel
with me. I did not breathe freely until I had taken it upstairs
and locked it in the bureau of my dressing-room.
"And now a word as to my household, Mr. Holmes, for I wish you to
thoroughly understand the situation. My groom and my page sleep
out of the house, and may be set aside altogether. I have three
maid-servants who have been with me a number of years and whose
absolute reliability is quite above suspicion. Another, Lucy
Parr, the second waiting-maid, has only been in my service a few
months. She came with an excellent character, however, and has
always given me satisfaction. She is a very pretty girl and has
attracted admirers who have occasionally hung about the place.
That is the only drawback which we have found to her, but we
believe her to be a thoroughly good girl in every way.
"So much for the servants. My family itself is so small that it
will not take me long to describe it. I am a widower and have an
only son, Arthur. He has been a disappointment to me, Mr.
Holmes--a grievous disappointment. I have no doubt that I am
myself to blame. People tell me that I have spoiled him. Very
likely I have. When my dear wife died I felt that he was all I
had to love. I could not bear to see the smile fade even for a
moment from his face. I have never denied him a wish. Perhaps it
would have been better for both of us had I been sterner, but I
meant it for the best.
"It was naturally my intention that he should succeed me in my
business, but he was not of a business turn. He was wild,
wayward, and, to speak the truth, I could not trust him in the
handling of large sums of money. When he was young he became a
member of an aristocratic club, and there, having charming
manners, he was soon the intimate of a number of men with long
purses and expensive habits. He learned to play heavily at cards
and to squander money on the turf, until he had again and again
to come to me and implore me to give him an advance upon his
allowance, that he might settle his debts of honour. He tried
more than once to break away from the dangerous company which he
was keeping, but each time the influence of his friend, Sir
George Burnwell, was enough to draw him back again.
"And, indeed, I could not wonder that such a man as Sir George
Burnwell should gain an influence over him, for he has frequently
brought him to my house, and I have found myself that I could
hardly resist the fascination of his manner. He is older than
Arthur, a man of the world to his finger-tips, one who had been
everywhere, seen everything, a brilliant talker, and a man of
great personal beauty. Yet when I think of him in cold blood, far
away from the glamour of his presence, I am convinced from his
cynical speech and the look which I have caught in his eyes that
he is one who should be deeply distrusted. So I think, and so,
too, thinks my little Mary, who has a woman's quick insight into
character.
"And now there is only she to be described. She is my niece; but
when my brother died five years ago and left her alone in the
world I adopted her, and have looked upon her ever since as my
daughter. She is a sunbeam in my house--sweet, loving, beautiful,
a wonderful manager and housekeeper, yet as tender and quiet and
gentle as a woman could be. She is my right hand. I do not know
what I could do without her. In only one matter has she ever gone
against my wishes. Twice my boy has asked her to marry him, for
he loves her devotedly, but each time she has refused him. I
think that if anyone could have drawn him into the right path it
would have been she, and that his marriage might have changed his
whole life; but now, alas! it is too late--forever too late!
"Now, Mr. Holmes, you know the people who live under my roof, and
I shall continue with my miserable story.
"When we were taking coffee in the drawing-room that night after
dinner, I told Arthur and Mary my experience, and of the precious
treasure which we had under our roof, suppressing only the name
of my client. Lucy Parr, who had brought in the coffee, had, I am
sure, left the room; but I cannot swear that the door was closed.
Mary and Arthur were much interested and wished to see the famous
coronet, but I thought it better not to disturb it.
"'Where have you put it?' asked Arthur.
"'In my own bureau.'
"'Well, I hope to goodness the house won't be burgled during the
night.' said he.
"'It is locked up,' I answered.
"'Oh, any old key will fit that bureau. When I was a youngster I
have opened it myself with the key of the box-room cupboard.'
"He often had a wild way of talking, so that I thought little of
what he said. He followed me to my room, however, that night with
a very grave face.
"'Look here, dad,' said he with his eyes cast down, 'can you let
me have 200 pounds?'
"'No, I cannot!' I answered sharply. 'I have been far too
generous with you in money matters.'
"'You have been very kind,' said he, 'but I must have this money,
or else I can never show my face inside the club again.'
"'And a very good thing, too!' I cried.
"'Yes, but you would not have me leave it a dishonoured man,'
said he. 'I could not bear the disgrace. I must raise the money
in some way, and if you will not let me have it, then I must try
other means.'
"I was very angry, for this was the third demand during the
month. 'You shall not have a farthing from me,' I cried, on which
he bowed and left the room without another word.
"When he was gone I unlocked my bureau, made sure that my
treasure was safe, and locked it again. Then I started to go
round the house to see that all was secure--a duty which I
usually leave to Mary but which I thought it well to perform
myself that night. As I came down the stairs I saw Mary herself
at the side window of the hall, which she closed and fastened as
I approached.
"'Tell me, dad,' said she, looking, I thought, a little
disturbed, 'did you give Lucy, the maid, leave to go out
to-night?'
"'Certainly not.'
"'She came in just now by the back door. I have no doubt that she
has only been to the side gate to see someone, but I think that
it is hardly safe and should be stopped.'
"'You must speak to her in the morning, or I will if you prefer
it. Are you sure that everything is fastened?'
"'Quite sure, dad.'
"'Then, good-night.' I kissed her and went up to my bedroom
again, where I was soon asleep.
"I am endeavouring to tell you everything, Mr. Holmes, which may
have any bearing upon the case, but I beg that you will question
me upon any point which I do not make clear."
"On the contrary, your statement is singularly lucid."
"I come to a part of my story now in which I should wish to be
particularly so. I am not a very heavy sleeper, and the anxiety
in my mind tended, no doubt, to make me even less so than usual.
About two in the morning, then, I was awakened by some sound in
the house. It had ceased ere I was wide awake, but it had left an
impression behind it as though a window had gently closed
somewhere. I lay listening with all my ears. Suddenly, to my
horror, there was a distinct sound of footsteps moving softly in
the next room. I slipped out of bed, all palpitating with fear,
and peeped round the corner of my dressing-room door.
"'Arthur!' I screamed, 'you villain! you thief! How dare you
touch that coronet?'
"The gas was half up, as I had left it, and my unhappy boy,
dressed only in his shirt and trousers, was standing beside the
light, holding the coronet in his hands. He appeared to be
wrenching at it, or bending it with all his strength. At my cry
he dropped it from his grasp and turned as pale as death. I
snatched it up and examined it. One of the gold corners, with
three of the beryls in it, was missing.
"'You blackguard!' I shouted, beside myself with rage. 'You have
destroyed it! You have dishonoured me forever! Where are the
jewels which you have stolen?'
"'Stolen!' he cried.
"'Yes, thief!' I roared, shaking him by the shoulder.
"'There are none missing. There cannot be any missing,' said he.
"'There are three missing. And you know where they are. Must I
call you a liar as well as a thief? Did I not see you trying to
tear off another piece?'
"'You have called me names enough,' said he, 'I will not stand it
any longer. I shall not say another word about this business,
since you have chosen to insult me. I will leave your house in
the morning and make my own way in the world.'
"'You shall leave it in the hands of the police!' I cried
half-mad with grief and rage. 'I shall have this matter probed to
the bottom.'
"'You shall learn nothing from me,' said he with a passion such
as I should not have thought was in his nature. 'If you choose to
call the police, let the police find what they can.'
"By this time the whole house was astir, for I had raised my
voice in my anger. Mary was the first to rush into my room, and,
at the sight of the coronet and of Arthur's face, she read the
whole story and, with a scream, fell down senseless on the
ground. I sent the house-maid for the police and put the
investigation into their hands at once. When the inspector and a
constable entered the house, Arthur, who had stood sullenly with
his arms folded, asked me whether it was my intention to charge
him with theft. I answered that it had ceased to be a private
matter, but had become a public one, since the ruined coronet was
national property. I was determined that the law should have its
way in everything.
"'At least,' said he, 'you will not have me arrested at once. It
would be to your advantage as well as mine if I might leave the
house for five minutes.'
"'That you may get away, or perhaps that you may conceal what you
have stolen,' said I. And then, realising the dreadful position
in which I was placed, I implored him to remember that not only
my honour but that of one who was far greater than I was at
stake; and that he threatened to raise a scandal which would
convulse the nation. He might avert it all if he would but tell
me what he had done with the three missing stones.
"'You may as well face the matter,' said I; 'you have been caught
in the act, and no confession could make your guilt more heinous.
If you but make such reparation as is in your power, by telling
us where the beryls are, all shall be forgiven and forgotten.'
"'Keep your forgiveness for those who ask for it,' he answered,
turning away from me with a sneer. I saw that he was too hardened
for any words of mine to influence him. There was but one way for
it. I called in the inspector and gave him into custody. A search
was made at once not only of his person but of his room and of
every portion of the house where he could possibly have concealed
the gems; but no trace of them could be found, nor would the
wretched boy open his mouth for all our persuasions and our
threats. This morning he was removed to a cell, and I, after
going through all the police formalities, have hurried round to
you to implore you to use your skill in unravelling the matter.
The police have openly confessed that they can at present make
nothing of it. You may go to any expense which you think
necessary. I have already offered a reward of 1000 pounds. My
God, what shall I do! I have lost my honour, my gems, and my son
in one night. Oh, what shall I do!"
He put a hand on either side of his head and rocked himself to
and fro, droning to himself like a child whose grief has got
beyond words.
Sherlock Holmes sat silent for some few minutes, with his brows
knitted and his eyes fixed upon the fire.
"Do you receive much company?" he asked.
"None save my partner with his family and an occasional friend of
Arthur's. Sir George Burnwell has been several times lately. No
one else, I think."
"Do you go out much in society?"
"Arthur does. Mary and I stay at home. We neither of us care for
it."
"That is unusual in a young girl."
"She is of a quiet nature. Besides, she is not so very young. She
is four-and-twenty."
"This matter, from what you say, seems to have been a shock to
her also."
"Terrible! She is even more affected than I."
"You have neither of you any doubt as to your son's guilt?"
"How can we have when I saw him with my own eyes with the coronet
in his hands."
"I hardly consider that a conclusive proof. Was the remainder of
the coronet at all injured?"
"Yes, it was twisted."
"Do you not think, then, that he might have been trying to
straighten it?"
"God bless you! You are doing what you can for him and for me.
But it is too heavy a task. What was he doing there at all? If
his purpose were innocent, why did he not say so?"
"Precisely. And if it were guilty, why did he not invent a lie?
His silence appears to me to cut both ways. There are several
singular points about the case. What did the police think of the
noise which awoke you from your sleep?"
"They considered that it might be caused by Arthur's closing his
bedroom door."
"A likely story! As if a man bent on felony would slam his door
so as to wake a household. What did they say, then, of the
disappearance of these gems?"
"They are still sounding the planking and probing the furniture
in the hope of finding them."
"Have they thought of looking outside the house?"
"Yes, they have shown extraordinary energy. The whole garden has
already been minutely examined."
"Now, my dear sir," said Holmes, "is it not obvious to you now
that this matter really strikes very much deeper than either you
or the police were at first inclined to think? It appeared to you
to be a simple case; to me it seems exceedingly complex. Consider
what is involved by your theory. You suppose that your son came
down from his bed, went, at great risk, to your dressing-room,
opened your bureau, took out your coronet, broke off by main
force a small portion of it, went off to some other place,
concealed three gems out of the thirty-nine, with such skill that
nobody can find them, and then returned with the other thirty-six
into the room in which he exposed himself to the greatest danger
of being discovered. I ask you now, is such a theory tenable?"
"But what other is there?" cried the banker with a gesture of
despair. "If his motives were innocent, why does he not explain
them?"
"It is our task to find that out," replied Holmes; "so now, if
you please, Mr. Holder, we will set off for Streatham together,
and devote an hour to glancing a little more closely into
details."
My friend insisted upon my accompanying them in their expedition,
which I was eager enough to do, for my curiosity and sympathy
were deeply stirred by the story to which we had listened. I
confess that the guilt of the banker's son appeared to me to be
as obvious as it did to his unhappy father, but still I had such
faith in Holmes' judgment that I felt that there must be some
grounds for hope as long as he was dissatisfied with the accepted
explanation. He hardly spoke a word the whole way out to the
southern suburb, but sat with his chin upon his breast and his
hat drawn over his eyes, sunk in the deepest thought. Our client
appeared to have taken fresh heart at the little glimpse of hope
which had been presented to him, and he even broke into a
desultory chat with me over his business affairs. A short railway
journey and a shorter walk brought us to Fairbank, the modest
residence of the great financier.
Fairbank was a good-sized square house of white stone, standing
back a little from the road. A double carriage-sweep, with a
snow-clad lawn, stretched down in front to two large iron gates
which closed the entrance. On the right side was a small wooden
thicket, which led into a narrow path between two neat hedges
stretching from the road to the kitchen door, and forming the
tradesmen's entrance. On the left ran a lane which led to the
stables, and was not itself within the grounds at all, being a
public, though little used, thoroughfare. Holmes left us standing
at the door and walked slowly all round the house, across the
front, down the tradesmen's path, and so round by the garden
behind into the stable lane. So long was he that Mr. Holder and I
went into the dining-room and waited by the fire until he should
return. We were sitting there in silence when the door opened and
a young lady came in. She was rather above the middle height,
slim, with dark hair and eyes, which seemed the darker against
the absolute pallor of her skin. I do not think that I have ever
seen such deadly paleness in a woman's face. Her lips, too, were
bloodless, but her eyes were flushed with crying. As she swept
silently into the room she impressed me with a greater sense of
grief than the banker had done in the morning, and it was the
more striking in her as she was evidently a woman of strong
character, with immense capacity for self-restraint. Disregarding
my presence, she went straight to her uncle and passed her hand
over his head with a sweet womanly caress.
"You have given orders that Arthur should be liberated, have you
not, dad?" she asked.
"No, no, my girl, the matter must be probed to the bottom."
"But I am so sure that he is innocent. You know what woman's
instincts are. I know that he has done no harm and that you will
be sorry for having acted so harshly."
"Why is he silent, then, if he is innocent?"
"Who knows? Perhaps because he was so angry that you should
suspect him."
"How could I help suspecting him, when I actually saw him with
the coronet in his hand?"
"Oh, but he had only picked it up to look at it. Oh, do, do take
my word for it that he is innocent. Let the matter drop and say
no more. It is so dreadful to think of our dear Arthur in
prison!"
"I shall never let it drop until the gems are found--never, Mary!
Your affection for Arthur blinds you as to the awful consequences
to me. Far from hushing the thing up, I have brought a gentleman
down from London to inquire more deeply into it."
"This gentleman?" she asked, facing round to me.
"No, his friend. He wished us to leave him alone. He is round in
the stable lane now."
"The stable lane?" She raised her dark eyebrows. "What can he
hope to find there? Ah! this, I suppose, is he. I trust, sir,
that you will succeed in proving, what I feel sure is the truth,
that my cousin Arthur is innocent of this crime."
"I fully share your opinion, and I trust, with you, that we may
prove it," returned Holmes, going back to the mat to knock the
snow from his shoes. "I believe I have the honour of addressing
Miss Mary Holder. Might I ask you a question or two?"
"Pray do, sir, if it may help to clear this horrible affair up."
"You heard nothing yourself last night?"
"Nothing, until my uncle here began to speak loudly. I heard
that, and I came down."
"You shut up the windows and doors the night before. Did you
fasten all the windows?"
"Yes."
"Were they all fastened this morning?"
"Yes."
"You have a maid who has a sweetheart? I think that you remarked
to your uncle last night that she had been out to see him?"
"Yes, and she was the girl who waited in the drawing-room, and
who may have heard uncle's remarks about the coronet."
"I see. You infer that she may have gone out to tell her
sweetheart, and that the two may have planned the robbery."
"But what is the good of all these vague theories," cried the
banker impatiently, "when I have told you that I saw Arthur with
the coronet in his hands?"
"Wait a little, Mr. Holder. We must come back to that. About this
girl, Miss Holder. You saw her return by the kitchen door, I
presume?"
"Yes; when I went to see if the door was fastened for the night I
met her slipping in. I saw the man, too, in the gloom."
"Do you know him?"
"Oh, yes! he is the green-grocer who brings our vegetables round.
His name is Francis Prosper."
"He stood," said Holmes, "to the left of the door--that is to
say, farther up the path than is necessary to reach the door?"
"Yes, he did."
"And he is a man with a wooden leg?"
Something like fear sprang up in the young lady's expressive
black eyes. "Why, you are like a magician," said she. "How do you
know that?" She smiled, but there was no answering smile in
Holmes' thin, eager face.
"I should be very glad now to go upstairs," said he. "I shall
probably wish to go over the outside of the house again. Perhaps
I had better take a look at the lower windows before I go up."
He walked swiftly round from one to the other, pausing only at
the large one which looked from the hall onto the stable lane.
This he opened and made a very careful examination of the sill
with his powerful magnifying lens. "Now we shall go upstairs,"
said he at last.
The banker's dressing-room was a plainly furnished little
chamber, with a grey carpet, a large bureau, and a long mirror.
Holmes went to the bureau first and looked hard at the lock.
"Which key was used to open it?" he asked.
"That which my son himself indicated--that of the cupboard of the
lumber-room."
"Have you it here?"
"That is it on the dressing-table."
Sherlock Holmes took it up and opened the bureau.
"It is a noiseless lock," said he. "It is no wonder that it did
not wake you. This case, I presume, contains the coronet. We must
have a look at it." He opened the case, and taking out the diadem
he laid it upon the table. It was a magnificent specimen of the
jeweller's art, and the thirty-six stones were the finest that I
have ever seen. At one side of the coronet was a cracked edge,
where a corner holding three gems had been torn away.
"Now, Mr. Holder," said Holmes, "here is the corner which
corresponds to that which has been so unfortunately lost. Might I
beg that you will break it off."
The banker recoiled in horror. "I should not dream of trying,"
said he.
"Then I will." Holmes suddenly bent his strength upon it, but
without result. "I feel it give a little," said he; "but, though
I am exceptionally strong in the fingers, it would take me all my
time to break it. An ordinary man could not do it. Now, what do
you think would happen if I did break it, Mr. Holder? There would
be a noise like a pistol shot. Do you tell me that all this
happened within a few yards of your bed and that you heard
nothing of it?"
"I do not know what to think. It is all dark to me."
"But perhaps it may grow lighter as we go. What do you think,
Miss Holder?"
"I confess that I still share my uncle's perplexity."
"Your son had no shoes or slippers on when you saw him?"
"He had nothing on save only his trousers and shirt."
"Thank you. We have certainly been favoured with extraordinary
luck during this inquiry, and it will be entirely our own fault
if we do not succeed in clearing the matter up. With your
permission, Mr. Holder, I shall now continue my investigations
outside."
He went alone, at his own request, for he explained that any
unnecessary footmarks might make his task more difficult. For an
hour or more he was at work, returning at last with his feet
heavy with snow and his features as inscrutable as ever.
"I think that I have seen now all that there is to see, Mr.
Holder," said he; "I can serve you best by returning to my
rooms."
"But the gems, Mr. Holmes. Where are they?"
"I cannot tell."
The banker wrung his hands. "I shall never see them again!" he
cried. "And my son? You give me hopes?"
"My opinion is in no way altered."
"Then, for God's sake, what was this dark business which was
acted in my house last night?"
"If you can call upon me at my Baker Street rooms to-morrow
morning between nine and ten I shall be happy to do what I can to
make it clearer. I understand that you give me carte blanche to
act for you, provided only that I get back the gems, and that you
place no limit on the sum I may draw."
"I would give my fortune to have them back."
"Very good. I shall look into the matter between this and then.
Good-bye; it is just possible that I may have to come over here
again before evening."
It was obvious to me that my companion's mind was now made up
about the case, although what his conclusions were was more than
I could even dimly imagine. Several times during our homeward
journey I endeavoured to sound him upon the point, but he always
glided away to some other topic, until at last I gave it over in
despair. It was not yet three when we found ourselves in our
rooms once more. He hurried to his chamber and was down again in
a few minutes dressed as a common loafer. With his collar turned
up, his shiny, seedy coat, his red cravat, and his worn boots, he
was a perfect sample of the class.
"I think that this should do," said he, glancing into the glass
above the fireplace. "I only wish that you could come with me,
Watson, but I fear that it won't do. I may be on the trail in
this matter, or I may be following a will-o'-the-wisp, but I
shall soon know which it is. I hope that I may be back in a few
hours." He cut a slice of beef from the joint upon the sideboard,
sandwiched it between two rounds of bread, and thrusting this
rude meal into his pocket he started off upon his expedition.
I had just finished my tea when he returned, evidently in
excellent spirits, swinging an old elastic-sided boot in his
hand. He chucked it down into a corner and helped himself to a
cup of tea.
"I only looked in as I passed," said he. "I am going right on."
"Where to?"
"Oh, to the other side of the West End. It may be some time
before I get back. Don't wait up for me in case I should be
late."
"How are you getting on?"
"Oh, so so. Nothing to complain of. I have been out to Streatham
since I saw you last, but I did not call at the house. It is a
very sweet little problem, and I would not have missed it for a
good deal. However, I must not sit gossiping here, but must get
these disreputable clothes off and return to my highly
respectable self."
I could see by his manner that he had stronger reasons for
satisfaction than his words alone would imply. His eyes twinkled,
and there was even a touch of colour upon his sallow cheeks. He
hastened upstairs, and a few minutes later I heard the slam of
the hall door, which told me that he was off once more upon his
congenial hunt.
I waited until midnight, but there was no sign of his return, so
I retired to my room. It was no uncommon thing for him to be away
for days and nights on end when he was hot upon a scent, so that
his lateness caused me no surprise. I do not know at what hour he
came in, but when I came down to breakfast in the morning there
he was with a cup of coffee in one hand and the paper in the
other, as fresh and trim as possible.
"You will excuse my beginning without you, Watson," said he, "but
you remember that our client has rather an early appointment this
morning."
"Why, it is after nine now," I answered. "I should not be
surprised if that were he. I thought I heard a ring."
It was, indeed, our friend the financier. I was shocked by the
change which had come over him, for his face which was naturally
of a broad and massive mould, was now pinched and fallen in,
while his hair seemed to me at least a shade whiter. He entered
with a weariness and lethargy which was even more painful than
his violence of the morning before, and he dropped heavily into
the armchair which I pushed forward for him.
"I do not know what I have done to be so severely tried," said
he. "Only two days ago I was a happy and prosperous man, without
a care in the world. Now I am left to a lonely and dishonoured
age. One sorrow comes close upon the heels of another. My niece,
Mary, has deserted me."
"Deserted you?"
"Yes. Her bed this morning had not been slept in, her room was
empty, and a note for me lay upon the hall table. I had said to
her last night, in sorrow and not in anger, that if she had
married my boy all might have been well with him. Perhaps it was
thoughtless of me to say so. It is to that remark that she refers
in this note:
"'MY DEAREST UNCLE:--I feel that I have brought trouble upon you,
and that if I had acted differently this terrible misfortune
might never have occurred. I cannot, with this thought in my
mind, ever again be happy under your roof, and I feel that I must
leave you forever. Do not worry about my future, for that is
provided for; and, above all, do not search for me, for it will
be fruitless labour and an ill-service to me. In life or in
death, I am ever your loving,--MARY.'
"What could she mean by that note, Mr. Holmes? Do you think it
points to suicide?"
"No, no, nothing of the kind. It is perhaps the best possible
solution. I trust, Mr. Holder, that you are nearing the end of
your troubles."
"Ha! You say so! You have heard something, Mr. Holmes; you have
learned something! Where are the gems?"
"You would not think 1000 pounds apiece an excessive sum for
them?"
"I would pay ten."
"That would be unnecessary. Three thousand will cover the matter.
And there is a little reward, I fancy. Have you your check-book?
Here is a pen. Better make it out for 4000 pounds."
With a dazed face the banker made out the required check. Holmes
walked over to his desk, took out a little triangular piece of
gold with three gems in it, and threw it down upon the table.
With a shriek of joy our client clutched it up.
"You have it!" he gasped. "I am saved! I am saved!"
The reaction of joy was as passionate as his grief had been, and
he hugged his recovered gems to his bosom.
"There is one other thing you owe, Mr. Holder," said Sherlock
Holmes rather sternly.
"Owe!" He caught up a pen. "Name the sum, and I will pay it."
"No, the debt is not to me. You owe a very humble apology to that
noble lad, your son, who has carried himself in this matter as I
should be proud to see my own son do, should I ever chance to
have one."
"Then it was not Arthur who took them?"
"I told you yesterday, and I repeat to-day, that it was not."
"You are sure of it! Then let us hurry to him at once to let him
know that the truth is known."
"He knows it already. When I had cleared it all up I had an
interview with him, and finding that he would not tell me the
story, I told it to him, on which he had to confess that I was
right and to add the very few details which were not yet quite
clear to me. Your news of this morning, however, may open his
lips."
"For heaven's sake, tell me, then, what is this extraordinary
mystery!"
"I will do so, and I will show you the steps by which I reached
it. And let me say to you, first, that which it is hardest for me
to say and for you to hear: there has been an understanding
between Sir George Burnwell and your niece Mary. They have now
fled together."
"My Mary? Impossible!"
"It is unfortunately more than possible; it is certain. Neither
you nor your son knew the true character of this man when you
admitted him into your family circle. He is one of the most
dangerous men in England--a ruined gambler, an absolutely
desperate villain, a man without heart or conscience. Your niece
knew nothing of such men. When he breathed his vows to her, as he
had done to a hundred before her, she flattered herself that she
alone had touched his heart. The devil knows best what he said,
but at least she became his tool and was in the habit of seeing
him nearly every evening."
"I cannot, and I will not, believe it!" cried the banker with an
ashen face.
"I will tell you, then, what occurred in your house last night.
Your niece, when you had, as she thought, gone to your room,
slipped down and talked to her lover through the window which
leads into the stable lane. His footmarks had pressed right
through the snow, so long had he stood there. She told him of the
coronet. His wicked lust for gold kindled at the news, and he
bent her to his will. I have no doubt that she loved you, but
there are women in whom the love of a lover extinguishes all
other loves, and I think that she must have been one. She had
hardly listened to his instructions when she saw you coming
downstairs, on which she closed the window rapidly and told you
about one of the servants' escapade with her wooden-legged lover,
which was all perfectly true.
"Your boy, Arthur, went to bed after his interview with you but
he slept badly on account of his uneasiness about his club debts.
In the middle of the night he heard a soft tread pass his door,
so he rose and, looking out, was surprised to see his cousin
walking very stealthily along the passage until she disappeared
into your dressing-room. Petrified with astonishment, the lad
slipped on some clothes and waited there in the dark to see what
would come of this strange affair. Presently she emerged from the
room again, and in the light of the passage-lamp your son saw
that she carried the precious coronet in her hands. She passed
down the stairs, and he, thrilling with horror, ran along and
slipped behind the curtain near your door, whence he could see
what passed in the hall beneath. He saw her stealthily open the
window, hand out the coronet to someone in the gloom, and then
closing it once more hurry back to her room, passing quite close
to where he stood hid behind the curtain.
"As long as she was on the scene he could not take any action
without a horrible exposure of the woman whom he loved. But the
instant that she was gone he realised how crushing a misfortune
this would be for you, and how all-important it was to set it
right. He rushed down, just as he was, in his bare feet, opened
the window, sprang out into the snow, and ran down the lane,
where he could see a dark figure in the moonlight. Sir George
Burnwell tried to get away, but Arthur caught him, and there was
a struggle between them, your lad tugging at one side of the
coronet, and his opponent at the other. In the scuffle, your son
struck Sir George and cut him over the eye. Then something
suddenly snapped, and your son, finding that he had the coronet
in his hands, rushed back, closed the window, ascended to your
room, and had just observed that the coronet had been twisted in
the struggle and was endeavouring to straighten it when you
appeared upon the scene."
"Is it possible?" gasped the banker.
"You then roused his anger by calling him names at a moment when
he felt that he had deserved your warmest thanks. He could not
explain the true state of affairs without betraying one who
certainly deserved little enough consideration at his hands. He
took the more chivalrous view, however, and preserved her
secret."
"And that was why she shrieked and fainted when she saw the
coronet," cried Mr. Holder. "Oh, my God! what a blind fool I have
been! And his asking to be allowed to go out for five minutes!
The dear fellow wanted to see if the missing piece were at the
scene of the struggle. How cruelly I have misjudged him!"
"When I arrived at the house," continued Holmes, "I at once went
very carefully round it to observe if there were any traces in
the snow which might help me. I knew that none had fallen since
the evening before, and also that there had been a strong frost
to preserve impressions. I passed along the tradesmen's path, but
found it all trampled down and indistinguishable. Just beyond it,
however, at the far side of the kitchen door, a woman had stood
and talked with a man, whose round impressions on one side showed
that he had a wooden leg. I could even tell that they had been
disturbed, for the woman had run back swiftly to the door, as was
shown by the deep toe and light heel marks, while Wooden-leg had
waited a little, and then had gone away. I thought at the time
that this might be the maid and her sweetheart, of whom you had
already spoken to me, and inquiry showed it was so. I passed
round the garden without seeing anything more than random tracks,
which I took to be the police; but when I got into the stable
lane a very long and complex story was written in the snow in
front of me.
"There was a double line of tracks of a booted man, and a second
double line which I saw with delight belonged to a man with naked
feet. I was at once convinced from what you had told me that the
latter was your son. The first had walked both ways, but the
other had run swiftly, and as his tread was marked in places over
the depression of the boot, it was obvious that he had passed
after the other. I followed them up and found they led to the
hall window, where Boots had worn all the snow away while
waiting. Then I walked to the other end, which was a hundred
yards or more down the lane. I saw where Boots had faced round,
where the snow was cut up as though there had been a struggle,
and, finally, where a few drops of blood had fallen, to show me
that I was not mistaken. Boots had then run down the lane, and
another little smudge of blood showed that it was he who had been
hurt. When he came to the highroad at the other end, I found that
the pavement had been cleared, so there was an end to that clue.
"On entering the house, however, I examined, as you remember, the
sill and framework of the hall window with my lens, and I could
at once see that someone had passed out. I could distinguish the
outline of an instep where the wet foot had been placed in coming
in. I was then beginning to be able to form an opinion as to what
had occurred. A man had waited outside the window; someone had
brought the gems; the deed had been overseen by your son; he had
pursued the thief; had struggled with him; they had each tugged
at the coronet, their united strength causing injuries which
neither alone could have effected. He had returned with the
prize, but had left a fragment in the grasp of his opponent. So
far I was clear. The question now was, who was the man and who
was it brought him the coronet?
"It is an old maxim of mine that when you have excluded the
impossible, whatever remains, however improbable, must be the
truth. Now, I knew that it was not you who had brought it down,
so there only remained your niece and the maids. But if it were
the maids, why should your son allow himself to be accused in
their place? There could be no possible reason. As he loved his
cousin, however, there was an excellent explanation why he should
retain her secret--the more so as the secret was a disgraceful
one. When I remembered that you had seen her at that window, and
how she had fainted on seeing the coronet again, my conjecture
became a certainty.
"And who could it be who was her confederate? A lover evidently,
for who else could outweigh the love and gratitude which she must
feel to you? I knew that you went out little, and that your
circle of friends was a very limited one. But among them was Sir
George Burnwell. I had heard of him before as being a man of evil
reputation among women. It must have been he who wore those boots
and retained the missing gems. Even though he knew that Arthur
had discovered him, he might still flatter himself that he was
safe, for the lad could not say a word without compromising his
own family.
"Well, your own good sense will suggest what measures I took
next. I went in the shape of a loafer to Sir George's house,
managed to pick up an acquaintance with his valet, learned that
his master had cut his head the night before, and, finally, at
the expense of six shillings, made all sure by buying a pair of
his cast-off shoes. With these I journeyed down to Streatham and
saw that they exactly fitted the tracks."
"I saw an ill-dressed vagabond in the lane yesterday evening,"
said Mr. Holder.
"Precisely. It was I. I found that I had my man, so I came home
and changed my clothes. It was a delicate part which I had to
play then, for I saw that a prosecution must be avoided to avert
scandal, and I knew that so astute a villain would see that our
hands were tied in the matter. I went and saw him. At first, of
course, he denied everything. But when I gave him every
particular that had occurred, he tried to bluster and took down a
life-preserver from the wall. I knew my man, however, and I
clapped a pistol to his head before he could strike. Then he
became a little more reasonable. I told him that we would give
him a price for the stones he held--1000 pounds apiece. That
brought out the first signs of grief that he had shown. 'Why,
dash it all!' said he, 'I've let them go at six hundred for the
three!' I soon managed to get the address of the receiver who had
them, on promising him that there would be no prosecution. Off I
set to him, and after much chaffering I got our stones at 1000
pounds apiece. Then I looked in upon your son, told him that all
was right, and eventually got to my bed about two o'clock, after
what I may call a really hard day's work."
"A day which has saved England from a great public scandal," said
the banker, rising. "Sir, I cannot find words to thank you, but
you shall not find me ungrateful for what you have done. Your
skill has indeed exceeded all that I have heard of it. And now I
must fly to my dear boy to apologise to him for the wrong which I
have done him. As to what you tell me of poor Mary, it goes to my
very heart. Not even your skill can inform me where she is now."
"I think that we may safely say," returned Holmes, "that she is
wherever Sir George Burnwell is. It is equally certain, too, that
whatever her sins are, they will soon receive a more than
sufficient punishment."
XII. THE ADVENTURE OF THE COPPER BEECHES
"To the man who loves art for its own sake," remarked Sherlock
Holmes, tossing aside the advertisement sheet of the Daily
Telegraph, "it is frequently in its least important and lowliest
manifestations that the keenest pleasure is to be derived. It is
pleasant to me to observe, Watson, that you have so far grasped
this truth that in these little records of our cases which you
have been good enough to draw up, and, I am bound to say,
occasionally to embellish, you have given prominence not so much
to the many causes célèbres and sensational trials in which I
have figured but rather to those incidents which may have been
trivial in themselves, but which have given room for those
faculties of deduction and of logical synthesis which I have made
my special province."
"And yet," said I, smiling, "I cannot quite hold myself absolved
from the charge of sensationalism which has been urged against my
records."
"You have erred, perhaps," he observed, taking up a glowing
cinder with the tongs and lighting with it the long cherry-wood
pipe which was wont to replace his clay when he was in a
disputatious rather than a meditative mood--"you have erred
perhaps in attempting to put colour and life into each of your
statements instead of confining yourself to the task of placing
upon record that severe reasoning from cause to effect which is
really the only notable feature about the thing."
"It seems to me that I have done you full justice in the matter,"
I remarked with some coldness, for I was repelled by the egotism
which I had more than once observed to be a strong factor in my
friend's singular character.
"No, it is not selfishness or conceit," said he, answering, as
was his wont, my thoughts rather than my words. "If I claim full
justice for my art, it is because it is an impersonal thing--a
thing beyond myself. Crime is common. Logic is rare. Therefore it
is upon the logic rather than upon the crime that you should
dwell. You have degraded what should have been a course of
lectures into a series of tales."
It was a cold morning of the early spring, and we sat after
breakfast on either side of a cheery fire in the old room at
Baker Street. A thick fog rolled down between the lines of
dun-coloured houses, and the opposing windows loomed like dark,
shapeless blurs through the heavy yellow wreaths. Our gas was lit
and shone on the white cloth and glimmer of china and metal, for
the table had not been cleared yet. Sherlock Holmes had been
silent all the morning, dipping continuously into the
advertisement columns of a succession of papers until at last,
having apparently given up his search, he had emerged in no very
sweet temper to lecture me upon my literary shortcomings.
"At the same time," he remarked after a pause, during which he
had sat puffing at his long pipe and gazing down into the fire,
"you can hardly be open to a charge of sensationalism, for out of
these cases which you have been so kind as to interest yourself
in, a fair proportion do not treat of crime, in its legal sense,
at all. The small matter in which I endeavoured to help the King
of Bohemia, the singular experience of Miss Mary Sutherland, the
problem connected with the man with the twisted lip, and the
incident of the noble bachelor, were all matters which are
outside the pale of the law. But in avoiding the sensational, I
fear that you may have bordered on the trivial."
"The end may have been so," I answered, "but the methods I hold
to have been novel and of interest."
"Pshaw, my dear fellow, what do the public, the great unobservant
public, who could hardly tell a weaver by his tooth or a
compositor by his left thumb, care about the finer shades of
analysis and deduction! But, indeed, if you are trivial, I cannot
blame you, for the days of the great cases are past. Man, or at
least criminal man, has lost all enterprise and originality. As
to my own little practice, it seems to be degenerating into an
agency for recovering lost lead pencils and giving advice to
young ladies from boarding-schools. I think that I have touched
bottom at last, however. This note I had this morning marks my
zero-point, I fancy. Read it!" He tossed a crumpled letter across
to me.
It was dated from Montague Place upon the preceding evening, and
ran thus:
"DEAR MR. HOLMES:--I am very anxious to consult you as to whether
I should or should not accept a situation which has been offered
to me as governess. I shall call at half-past ten to-morrow if I
do not inconvenience you. Yours faithfully,
"VIOLET HUNTER."
"Do you know the young lady?" I asked.
"Not I."
"It is half-past ten now."
"Yes, and I have no doubt that is her ring."
"It may turn out to be of more interest than you think. You
remember that the affair of the blue carbuncle, which appeared to
be a mere whim at first, developed into a serious investigation.
It may be so in this case, also."
"Well, let us hope so. But our doubts will very soon be solved,
for here, unless I am much mistaken, is the person in question."
As he spoke the door opened and a young lady entered the room.
She was plainly but neatly dressed, with a bright, quick face,
freckled like a plover's egg, and with the brisk manner of a
woman who has had her own way to make in the world.
"You will excuse my troubling you, I am sure," said she, as my
companion rose to greet her, "but I have had a very strange
experience, and as I have no parents or relations of any sort
from whom I could ask advice, I thought that perhaps you would be
kind enough to tell me what I should do."
"Pray take a seat, Miss Hunter. I shall be happy to do anything
that I can to serve you."
I could see that Holmes was favourably impressed by the manner
and speech of his new client. He looked her over in his searching
fashion, and then composed himself, with his lids drooping and
his finger-tips together, to listen to her story.
"I have been a governess for five years," said she, "in the
family of Colonel Spence Munro, but two months ago the colonel
received an appointment at Halifax, in Nova Scotia, and took his
children over to America with him, so that I found myself without
a situation. I advertised, and I answered advertisements, but
without success. At last the little money which I had saved began
to run short, and I was at my wit's end as to what I should do.
"There is a well-known agency for governesses in the West End
called Westaway's, and there I used to call about once a week in
order to see whether anything had turned up which might suit me.
Westaway was the name of the founder of the business, but it is
really managed by Miss Stoper. She sits in her own little office,
and the ladies who are seeking employment wait in an anteroom,
and are then shown in one by one, when she consults her ledgers
and sees whether she has anything which would suit them.
"Well, when I called last week I was shown into the little office
as usual, but I found that Miss Stoper was not alone. A
prodigiously stout man with a very smiling face and a great heavy
chin which rolled down in fold upon fold over his throat sat at
her elbow with a pair of glasses on his nose, looking very
earnestly at the ladies who entered. As I came in he gave quite a
jump in his chair and turned quickly to Miss Stoper.
"'That will do,' said he; 'I could not ask for anything better.
Capital! capital!' He seemed quite enthusiastic and rubbed his
hands together in the most genial fashion. He was such a
comfortable-looking man that it was quite a pleasure to look at
him.
"'You are looking for a situation, miss?' he asked.
"'Yes, sir.'
"'As governess?'
"'Yes, sir.'
"'And what salary do you ask?'
"'I had 4 pounds a month in my last place with Colonel Spence
Munro.'
"'Oh, tut, tut! sweating--rank sweating!' he cried, throwing his
fat hands out into the air like a man who is in a boiling
passion. 'How could anyone offer so pitiful a sum to a lady with
such attractions and accomplishments?'
"'My accomplishments, sir, may be less than you imagine,' said I.
'A little French, a little German, music, and drawing--'
"'Tut, tut!' he cried. 'This is all quite beside the question.
The point is, have you or have you not the bearing and deportment
of a lady? There it is in a nutshell. If you have not, you are
not fitted for the rearing of a child who may some day play a
considerable part in the history of the country. But if you have
why, then, how could any gentleman ask you to condescend to
accept anything under the three figures? Your salary with me,
madam, would commence at 100 pounds a year.'
"You may imagine, Mr. Holmes, that to me, destitute as I was,
such an offer seemed almost too good to be true. The gentleman,
however, seeing perhaps the look of incredulity upon my face,
opened a pocket-book and took out a note.
"'It is also my custom,' said he, smiling in the most pleasant
fashion until his eyes were just two little shining slits amid
the white creases of his face, 'to advance to my young ladies
half their salary beforehand, so that they may meet any little
expenses of their journey and their wardrobe.'
"It seemed to me that I had never met so fascinating and so
thoughtful a man. As I was already in debt to my tradesmen, the
advance was a great convenience, and yet there was something
unnatural about the whole transaction which made me wish to know
a little more before I quite committed myself.
"'May I ask where you live, sir?' said I.
"'Hampshire. Charming rural place. The Copper Beeches, five miles
on the far side of Winchester. It is the most lovely country, my
dear young lady, and the dearest old country-house.'
"'And my duties, sir? I should be glad to know what they would
be.'
"'One child--one dear little romper just six years old. Oh, if
you could see him killing cockroaches with a slipper! Smack!
smack! smack! Three gone before you could wink!' He leaned back
in his chair and laughed his eyes into his head again.
"I was a little startled at the nature of the child's amusement,
but the father's laughter made me think that perhaps he was
joking.
"'My sole duties, then,' I asked, 'are to take charge of a single
child?'
"'No, no, not the sole, not the sole, my dear young lady,' he
cried. 'Your duty would be, as I am sure your good sense would
suggest, to obey any little commands my wife might give, provided
always that they were such commands as a lady might with
propriety obey. You see no difficulty, heh?'
"'I should be happy to make myself useful.'
"'Quite so. In dress now, for example. We are faddy people, you
know--faddy but kind-hearted. If you were asked to wear any dress
which we might give you, you would not object to our little whim.
Heh?'
"'No,' said I, considerably astonished at his words.
"'Or to sit here, or sit there, that would not be offensive to
you?'
"'Oh, no.'
"'Or to cut your hair quite short before you come to us?'
"I could hardly believe my ears. As you may observe, Mr. Holmes,
my hair is somewhat luxuriant, and of a rather peculiar tint of
chestnut. It has been considered artistic. I could not dream of
sacrificing it in this offhand fashion.
"'I am afraid that that is quite impossible,' said I. He had been
watching me eagerly out of his small eyes, and I could see a
shadow pass over his face as I spoke.
"'I am afraid that it is quite essential,' said he. 'It is a
little fancy of my wife's, and ladies' fancies, you know, madam,
ladies' fancies must be consulted. And so you won't cut your
hair?'
"'No, sir, I really could not,' I answered firmly.
"'Ah, very well; then that quite settles the matter. It is a
pity, because in other respects you would really have done very
nicely. In that case, Miss Stoper, I had best inspect a few more
of your young ladies.'
"The manageress had sat all this while busy with her papers
without a word to either of us, but she glanced at me now with so
much annoyance upon her face that I could not help suspecting
that she had lost a handsome commission through my refusal.
"'Do you desire your name to be kept upon the books?' she asked.
"'If you please, Miss Stoper.'
"'Well, really, it seems rather useless, since you refuse the
most excellent offers in this fashion,' said she sharply. 'You
can hardly expect us to exert ourselves to find another such
opening for you. Good-day to you, Miss Hunter.' She struck a gong
upon the table, and I was shown out by the page.
"Well, Mr. Holmes, when I got back to my lodgings and found
little enough in the cupboard, and two or three bills upon the
table, I began to ask myself whether I had not done a very
foolish thing. After all, if these people had strange fads and
expected obedience on the most extraordinary matters, they were
at least ready to pay for their eccentricity. Very few
governesses in England are getting 100 pounds a year. Besides,
what use was my hair to me? Many people are improved by wearing
it short and perhaps I should be among the number. Next day I was
inclined to think that I had made a mistake, and by the day after
I was sure of it. I had almost overcome my pride so far as to go
back to the agency and inquire whether the place was still open
when I received this letter from the gentleman himself. I have it
here and I will read it to you:
"'The Copper Beeches, near Winchester.
"'DEAR MISS HUNTER:--Miss Stoper has very kindly given me your
address, and I write from here to ask you whether you have
reconsidered your decision. My wife is very anxious that you
should come, for she has been much attracted by my description of
you. We are willing to give 30 pounds a quarter, or 120 pounds a
year, so as to recompense you for any little inconvenience which
our fads may cause you. They are not very exacting, after all. My
wife is fond of a particular shade of electric blue and would
like you to wear such a dress indoors in the morning. You need
not, however, go to the expense of purchasing one, as we have one
belonging to my dear daughter Alice (now in Philadelphia), which
would, I should think, fit you very well. Then, as to sitting
here or there, or amusing yourself in any manner indicated, that
need cause you no inconvenience. As regards your hair, it is no
doubt a pity, especially as I could not help remarking its beauty
during our short interview, but I am afraid that I must remain
firm upon this point, and I only hope that the increased salary
may recompense you for the loss. Your duties, as far as the child
is concerned, are very light. Now do try to come, and I shall
meet you with the dog-cart at Winchester. Let me know your train.
Yours faithfully, JEPHRO RUCASTLE.'
"That is the letter which I have just received, Mr. Holmes, and
my mind is made up that I will accept it. I thought, however,
that before taking the final step I should like to submit the
whole matter to your consideration."
"Well, Miss Hunter, if your mind is made up, that settles the
question," said Holmes, smiling.
"But you would not advise me to refuse?"
"I confess that it is not the situation which I should like to
see a sister of mine apply for."
"What is the meaning of it all, Mr. Holmes?"
"Ah, I have no data. I cannot tell. Perhaps you have yourself
formed some opinion?"
"Well, there seems to me to be only one possible solution. Mr.
Rucastle seemed to be a very kind, good-natured man. Is it not
possible that his wife is a lunatic, that he desires to keep the
matter quiet for fear she should be taken to an asylum, and that
he humours her fancies in every way in order to prevent an
outbreak?"
"That is a possible solution--in fact, as matters stand, it is
the most probable one. But in any case it does not seem to be a
nice household for a young lady."
"But the money, Mr. Holmes, the money!"
"Well, yes, of course the pay is good--too good. That is what
makes me uneasy. Why should they give you 120 pounds a year, when
they could have their pick for 40 pounds? There must be some
strong reason behind."
"I thought that if I told you the circumstances you would
understand afterwards if I wanted your help. I should feel so
much stronger if I felt that you were at the back of me."
"Oh, you may carry that feeling away with you. I assure you that
your little problem promises to be the most interesting which has
come my way for some months. There is something distinctly novel
about some of the features. If you should find yourself in doubt
or in danger--"
"Danger! What danger do you foresee?"
Holmes shook his head gravely. "It would cease to be a danger if
we could define it," said he. "But at any time, day or night, a
telegram would bring me down to your help."
"That is enough." She rose briskly from her chair with the
anxiety all swept from her face. "I shall go down to Hampshire
quite easy in my mind now. I shall write to Mr. Rucastle at once,
sacrifice my poor hair to-night, and start for Winchester
to-morrow." With a few grateful words to Holmes she bade us both
good-night and bustled off upon her way.
"At least," said I as we heard her quick, firm steps descending
the stairs, "she seems to be a young lady who is very well able
to take care of herself."
"And she would need to be," said Holmes gravely. "I am much
mistaken if we do not hear from her before many days are past."
It was not very long before my friend's prediction was fulfilled.
A fortnight went by, during which I frequently found my thoughts
turning in her direction and wondering what strange side-alley of
human experience this lonely woman had strayed into. The unusual
salary, the curious conditions, the light duties, all pointed to
something abnormal, though whether a fad or a plot, or whether
the man were a philanthropist or a villain, it was quite beyond
my powers to determine. As to Holmes, I observed that he sat
frequently for half an hour on end, with knitted brows and an
abstracted air, but he swept the matter away with a wave of his
hand when I mentioned it. "Data! data! data!" he cried
impatiently. "I can't make bricks without clay." And yet he would
always wind up by muttering that no sister of his should ever
have accepted such a situation.
The telegram which we eventually received came late one night
just as I was thinking of turning in and Holmes was settling down
to one of those all-night chemical researches which he frequently
indulged in, when I would leave him stooping over a retort and a
test-tube at night and find him in the same position when I came
down to breakfast in the morning. He opened the yellow envelope,
and then, glancing at the message, threw it across to me.
"Just look up the trains in Bradshaw," said he, and turned back
to his chemical studies.
The summons was a brief and urgent one.
"Please be at the Black Swan Hotel at Winchester at midday
to-morrow," it said. "Do come! I am at my wit's end. HUNTER."
"Will you come with me?" asked Holmes, glancing up.
"I should wish to."
"Just look it up, then."
"There is a train at half-past nine," said I, glancing over my
Bradshaw. "It is due at Winchester at 11:30."
"That will do very nicely. Then perhaps I had better postpone my
analysis of the acetones, as we may need to be at our best in the
morning."
By eleven o'clock the next day we were well upon our way to the
old English capital. Holmes had been buried in the morning papers
all the way down, but after we had passed the Hampshire border he
threw them down and began to admire the scenery. It was an ideal
spring day, a light blue sky, flecked with little fleecy white
clouds drifting across from west to east. The sun was shining
very brightly, and yet there was an exhilarating nip in the air,
which set an edge to a man's energy. All over the countryside,
away to the rolling hills around Aldershot, the little red and
grey roofs of the farm-steadings peeped out from amid the light
green of the new foliage.
"Are they not fresh and beautiful?" I cried with all the
enthusiasm of a man fresh from the fogs of Baker Street.
But Holmes shook his head gravely.
"Do you know, Watson," said he, "that it is one of the curses of
a mind with a turn like mine that I must look at everything with
reference to my own special subject. You look at these scattered
houses, and you are impressed by their beauty. I look at them,
and the only thought which comes to me is a feeling of their
isolation and of the impunity with which crime may be committed
there."
"Good heavens!" I cried. "Who would associate crime with these
dear old homesteads?"
"They always fill me with a certain horror. It is my belief,
Watson, founded upon my experience, that the lowest and vilest
alleys in London do not present a more dreadful record of sin
than does the smiling and beautiful countryside."
"You horrify me!"
"But the reason is very obvious. The pressure of public opinion
can do in the town what the law cannot accomplish. There is no
lane so vile that the scream of a tortured child, or the thud of
a drunkard's blow, does not beget sympathy and indignation among
the neighbours, and then the whole machinery of justice is ever
so close that a word of complaint can set it going, and there is
but a step between the crime and the dock. But look at these
lonely houses, each in its own fields, filled for the most part
with poor ignorant folk who know little of the law. Think of the
deeds of hellish cruelty, the hidden wickedness which may go on,
year in, year out, in such places, and none the wiser. Had this
lady who appeals to us for help gone to live in Winchester, I
should never have had a fear for her. It is the five miles of
country which makes the danger. Still, it is clear that she is
not personally threatened."
"No. If she can come to Winchester to meet us she can get away."
"Quite so. She has her freedom."
"What CAN be the matter, then? Can you suggest no explanation?"
"I have devised seven separate explanations, each of which would
cover the facts as far as we know them. But which of these is
correct can only be determined by the fresh information which we
shall no doubt find waiting for us. Well, there is the tower of
the cathedral, and we shall soon learn all that Miss Hunter has
to tell."
The Black Swan is an inn of repute in the High Street, at no
distance from the station, and there we found the young lady
waiting for us. She had engaged a sitting-room, and our lunch
awaited us upon the table.
"I am so delighted that you have come," she said earnestly. "It
is so very kind of you both; but indeed I do not know what I
should do. Your advice will be altogether invaluable to me."
"Pray tell us what has happened to you."
"I will do so, and I must be quick, for I have promised Mr.
Rucastle to be back before three. I got his leave to come into
town this morning, though he little knew for what purpose."
"Let us have everything in its due order." Holmes thrust his long
thin legs out towards the fire and composed himself to listen.
"In the first place, I may say that I have met, on the whole,
with no actual ill-treatment from Mr. and Mrs. Rucastle. It is
only fair to them to say that. But I cannot understand them, and
I am not easy in my mind about them."
"What can you not understand?"
"Their reasons for their conduct. But you shall have it all just
as it occurred. When I came down, Mr. Rucastle met me here and
drove me in his dog-cart to the Copper Beeches. It is, as he
said, beautifully situated, but it is not beautiful in itself,
for it is a large square block of a house, whitewashed, but all
stained and streaked with damp and bad weather. There are grounds
round it, woods on three sides, and on the fourth a field which
slopes down to the Southampton highroad, which curves past about
a hundred yards from the front door. This ground in front belongs
to the house, but the woods all round are part of Lord
Southerton's preserves. A clump of copper beeches immediately in
front of the hall door has given its name to the place.
"I was driven over by my employer, who was as amiable as ever,
and was introduced by him that evening to his wife and the child.
There was no truth, Mr. Holmes, in the conjecture which seemed to
us to be probable in your rooms at Baker Street. Mrs. Rucastle is
not mad. I found her to be a silent, pale-faced woman, much
younger than her husband, not more than thirty, I should think,
while he can hardly be less than forty-five. From their
conversation I have gathered that they have been married about
seven years, that he was a widower, and that his only child by
the first wife was the daughter who has gone to Philadelphia. Mr.
Rucastle told me in private that the reason why she had left them
was that she had an unreasoning aversion to her stepmother. As
the daughter could not have been less than twenty, I can quite
imagine that her position must have been uncomfortable with her
father's young wife.
"Mrs. Rucastle seemed to me to be colourless in mind as well as
in feature. She impressed me neither favourably nor the reverse.
She was a nonentity. It was easy to see that she was passionately
devoted both to her husband and to her little son. Her light grey
eyes wandered continually from one to the other, noting every
little want and forestalling it if possible. He was kind to her
also in his bluff, boisterous fashion, and on the whole they
seemed to be a happy couple. And yet she had some secret sorrow,
this woman. She would often be lost in deep thought, with the
saddest look upon her face. More than once I have surprised her
in tears. I have thought sometimes that it was the disposition of
her child which weighed upon her mind, for I have never met so
utterly spoiled and so ill-natured a little creature. He is small
for his age, with a head which is quite disproportionately large.
His whole life appears to be spent in an alternation between
savage fits of passion and gloomy intervals of sulking. Giving
pain to any creature weaker than himself seems to be his one idea
of amusement, and he shows quite remarkable talent in planning
the capture of mice, little birds, and insects. But I would
rather not talk about the creature, Mr. Holmes, and, indeed, he
has little to do with my story."
"I am glad of all details," remarked my friend, "whether they
seem to you to be relevant or not."
"I shall try not to miss anything of importance. The one
unpleasant thing about the house, which struck me at once, was
the appearance and conduct of the servants. There are only two, a
man and his wife. Toller, for that is his name, is a rough,
uncouth man, with grizzled hair and whiskers, and a perpetual
smell of drink. Twice since I have been with them he has been
quite drunk, and yet Mr. Rucastle seemed to take no notice of it.
His wife is a very tall and strong woman with a sour face, as
silent as Mrs. Rucastle and much less amiable. They are a most
unpleasant couple, but fortunately I spend most of my time in the
nursery and my own room, which are next to each other in one
corner of the building.
"For two days after my arrival at the Copper Beeches my life was
very quiet; on the third, Mrs. Rucastle came down just after
breakfast and whispered something to her husband.
"'Oh, yes,' said he, turning to me, 'we are very much obliged to
you, Miss Hunter, for falling in with our whims so far as to cut
your hair. I assure you that it has not detracted in the tiniest
iota from your appearance. We shall now see how the electric-blue
dress will become you. You will find it laid out upon the bed in
your room, and if you would be so good as to put it on we should
both be extremely obliged.'
"The dress which I found waiting for me was of a peculiar shade
of blue. It was of excellent material, a sort of beige, but it
bore unmistakable signs of having been worn before. It could not
have been a better fit if I had been measured for it. Both Mr.
and Mrs. Rucastle expressed a delight at the look of it, which
seemed quite exaggerated in its vehemence. They were waiting for
me in the drawing-room, which is a very large room, stretching
along the entire front of the house, with three long windows
reaching down to the floor. A chair had been placed close to the
central window, with its back turned towards it. In this I was
asked to sit, and then Mr. Rucastle, walking up and down on the
other side of the room, began to tell me a series of the funniest
stories that I have ever listened to. You cannot imagine how
comical he was, and I laughed until I was quite weary. Mrs.
Rucastle, however, who has evidently no sense of humour, never so
much as smiled, but sat with her hands in her lap, and a sad,
anxious look upon her face. After an hour or so, Mr. Rucastle
suddenly remarked that it was time to commence the duties of the
day, and that I might change my dress and go to little Edward in
the nursery.
"Two days later this same performance was gone through under
exactly similar circumstances. Again I changed my dress, again I
sat in the window, and again I laughed very heartily at the funny
stories of which my employer had an immense répertoire, and which
he told inimitably. Then he handed me a yellow-backed novel, and
moving my chair a little sideways, that my own shadow might not
fall upon the page, he begged me to read aloud to him. I read for
about ten minutes, beginning in the heart of a chapter, and then
suddenly, in the middle of a sentence, he ordered me to cease and
to change my dress.
"You can easily imagine, Mr. Holmes, how curious I became as to
what the meaning of this extraordinary performance could possibly
be. They were always very careful, I observed, to turn my face
away from the window, so that I became consumed with the desire
to see what was going on behind my back. At first it seemed to be
impossible, but I soon devised a means. My hand-mirror had been
broken, so a happy thought seized me, and I concealed a piece of
the glass in my handkerchief. On the next occasion, in the midst
of my laughter, I put my handkerchief up to my eyes, and was able
with a little management to see all that there was behind me. I
confess that I was disappointed. There was nothing. At least that
was my first impression. At the second glance, however, I
perceived that there was a man standing in the Southampton Road,
a small bearded man in a grey suit, who seemed to be looking in
my direction. The road is an important highway, and there are
usually people there. This man, however, was leaning against the
railings which bordered our field and was looking earnestly up. I
lowered my handkerchief and glanced at Mrs. Rucastle to find her
eyes fixed upon me with a most searching gaze. She said nothing,
but I am convinced that she had divined that I had a mirror in my
hand and had seen what was behind me. She rose at once.
"'Jephro,' said she, 'there is an impertinent fellow upon the
road there who stares up at Miss Hunter.'
"'No friend of yours, Miss Hunter?' he asked.
"'No, I know no one in these parts.'
"'Dear me! How very impertinent! Kindly turn round and motion to
him to go away.'
"'Surely it would be better to take no notice.'
"'No, no, we should have him loitering here always. Kindly turn
round and wave him away like that.'
"I did as I was told, and at the same instant Mrs. Rucastle drew
down the blind. That was a week ago, and from that time I have
not sat again in the window, nor have I worn the blue dress, nor
seen the man in the road."
"Pray continue," said Holmes. "Your narrative promises to be a
most interesting one."
"You will find it rather disconnected, I fear, and there may
prove to be little relation between the different incidents of
which I speak. On the very first day that I was at the Copper
Beeches, Mr. Rucastle took me to a small outhouse which stands
near the kitchen door. As we approached it I heard the sharp
rattling of a chain, and the sound as of a large animal moving
about.
"'Look in here!' said Mr. Rucastle, showing me a slit between two
planks. 'Is he not a beauty?'
"I looked through and was conscious of two glowing eyes, and of a
vague figure huddled up in the darkness.
"'Don't be frightened,' said my employer, laughing at the start
which I had given. 'It's only Carlo, my mastiff. I call him mine,
but really old Toller, my groom, is the only man who can do
anything with him. We feed him once a day, and not too much then,
so that he is always as keen as mustard. Toller lets him loose
every night, and God help the trespasser whom he lays his fangs
upon. For goodness' sake don't you ever on any pretext set your
foot over the threshold at night, for it's as much as your life
is worth.'
"The warning was no idle one, for two nights later I happened to
look out of my bedroom window about two o'clock in the morning.
It was a beautiful moonlight night, and the lawn in front of the
house was silvered over and almost as bright as day. I was
standing, rapt in the peaceful beauty of the scene, when I was
aware that something was moving under the shadow of the copper
beeches. As it emerged into the moonshine I saw what it was. It
was a giant dog, as large as a calf, tawny tinted, with hanging
jowl, black muzzle, and huge projecting bones. It walked slowly
across the lawn and vanished into the shadow upon the other side.
That dreadful sentinel sent a chill to my heart which I do not
think that any burglar could have done.
"And now I have a very strange experience to tell you. I had, as
you know, cut off my hair in London, and I had placed it in a
great coil at the bottom of my trunk. One evening, after the
child was in bed, I began to amuse myself by examining the
furniture of my room and by rearranging my own little things.
There was an old chest of drawers in the room, the two upper ones
empty and open, the lower one locked. I had filled the first two
with my linen, and as I had still much to pack away I was
naturally annoyed at not having the use of the third drawer. It
struck me that it might have been fastened by a mere oversight,
so I took out my bunch of keys and tried to open it. The very
first key fitted to perfection, and I drew the drawer open. There
was only one thing in it, but I am sure that you would never
guess what it was. It was my coil of hair.
"I took it up and examined it. It was of the same peculiar tint,
and the same thickness. But then the impossibility of the thing
obtruded itself upon me. How could my hair have been locked in
the drawer? With trembling hands I undid my trunk, turned out the
contents, and drew from the bottom my own hair. I laid the two
tresses together, and I assure you that they were identical. Was
it not extraordinary? Puzzle as I would, I could make nothing at
all of what it meant. I returned the strange hair to the drawer,
and I said nothing of the matter to the Rucastles as I felt that
I had put myself in the wrong by opening a drawer which they had
locked.
"I am naturally observant, as you may have remarked, Mr. Holmes,
and I soon had a pretty good plan of the whole house in my head.
There was one wing, however, which appeared not to be inhabited
at all. A door which faced that which led into the quarters of
the Tollers opened into this suite, but it was invariably locked.
One day, however, as I ascended the stair, I met Mr. Rucastle
coming out through this door, his keys in his hand, and a look on
his face which made him a very different person to the round,
jovial man to whom I was accustomed. His cheeks were red, his
brow was all crinkled with anger, and the veins stood out at his
temples with passion. He locked the door and hurried past me
without a word or a look.
"This aroused my curiosity, so when I went out for a walk in the
grounds with my charge, I strolled round to the side from which I
could see the windows of this part of the house. There were four
of them in a row, three of which were simply dirty, while the
fourth was shuttered up. They were evidently all deserted. As I
strolled up and down, glancing at them occasionally, Mr. Rucastle
came out to me, looking as merry and jovial as ever.
"'Ah!' said he, 'you must not think me rude if I passed you
without a word, my dear young lady. I was preoccupied with
business matters.'
"I assured him that I was not offended. 'By the way,' said I,
'you seem to have quite a suite of spare rooms up there, and one
of them has the shutters up.'
"He looked surprised and, as it seemed to me, a little startled
at my remark.
"'Photography is one of my hobbies,' said he. 'I have made my
dark room up there. But, dear me! what an observant young lady we
have come upon. Who would have believed it? Who would have ever
believed it?' He spoke in a jesting tone, but there was no jest
in his eyes as he looked at me. I read suspicion there and
annoyance, but no jest.
"Well, Mr. Holmes, from the moment that I understood that there
was something about that suite of rooms which I was not to know,
I was all on fire to go over them. It was not mere curiosity,
though I have my share of that. It was more a feeling of duty--a
feeling that some good might come from my penetrating to this
place. They talk of woman's instinct; perhaps it was woman's
instinct which gave me that feeling. At any rate, it was there,
and I was keenly on the lookout for any chance to pass the
forbidden door.
"It was only yesterday that the chance came. I may tell you that,
besides Mr. Rucastle, both Toller and his wife find something to
do in these deserted rooms, and I once saw him carrying a large
black linen bag with him through the door. Recently he has been
drinking hard, and yesterday evening he was very drunk; and when
I came upstairs there was the key in the door. I have no doubt at
all that he had left it there. Mr. and Mrs. Rucastle were both
downstairs, and the child was with them, so that I had an
admirable opportunity. I turned the key gently in the lock,
opened the door, and slipped through.
"There was a little passage in front of me, unpapered and
uncarpeted, which turned at a right angle at the farther end.
Round this corner were three doors in a line, the first and third
of which were open. They each led into an empty room, dusty and
cheerless, with two windows in the one and one in the other, so
thick with dirt that the evening light glimmered dimly through
them. The centre door was closed, and across the outside of it
had been fastened one of the broad bars of an iron bed, padlocked
at one end to a ring in the wall, and fastened at the other with
stout cord. The door itself was locked as well, and the key was
not there. This barricaded door corresponded clearly with the
shuttered window outside, and yet I could see by the glimmer from
beneath it that the room was not in darkness. Evidently there was
a skylight which let in light from above. As I stood in the
passage gazing at the sinister door and wondering what secret it
might veil, I suddenly heard the sound of steps within the room
and saw a shadow pass backward and forward against the little
slit of dim light which shone out from under the door. A mad,
unreasoning terror rose up in me at the sight, Mr. Holmes. My
overstrung nerves failed me suddenly, and I turned and ran--ran
as though some dreadful hand were behind me clutching at the
skirt of my dress. I rushed down the passage, through the door,
and straight into the arms of Mr. Rucastle, who was waiting
outside.
"'So,' said he, smiling, 'it was you, then. I thought that it
must be when I saw the door open.'
"'Oh, I am so frightened!' I panted.
"'My dear young lady! my dear young lady!'--you cannot think how
caressing and soothing his manner was--'and what has frightened
you, my dear young lady?'
"But his voice was just a little too coaxing. He overdid it. I
was keenly on my guard against him.
"'I was foolish enough to go into the empty wing,' I answered.
'But it is so lonely and eerie in this dim light that I was
frightened and ran out again. Oh, it is so dreadfully still in
there!'
"'Only that?' said he, looking at me keenly.
"'Why, what did you think?' I asked.
"'Why do you think that I lock this door?'
"'I am sure that I do not know.'
"'It is to keep people out who have no business there. Do you
see?' He was still smiling in the most amiable manner.
"'I am sure if I had known--'
"'Well, then, you know now. And if you ever put your foot over
that threshold again'--here in an instant the smile hardened into
a grin of rage, and he glared down at me with the face of a
demon--'I'll throw you to the mastiff.'
"I was so terrified that I do not know what I did. I suppose that
I must have rushed past him into my room. I remember nothing
until I found myself lying on my bed trembling all over. Then I
thought of you, Mr. Holmes. I could not live there longer without
some advice. I was frightened of the house, of the man, of the
woman, of the servants, even of the child. They were all horrible
to me. If I could only bring you down all would be well. Of
course I might have fled from the house, but my curiosity was
almost as strong as my fears. My mind was soon made up. I would
send you a wire. I put on my hat and cloak, went down to the
office, which is about half a mile from the house, and then
returned, feeling very much easier. A horrible doubt came into my
mind as I approached the door lest the dog might be loose, but I
remembered that Toller had drunk himself into a state of
insensibility that evening, and I knew that he was the only one
in the household who had any influence with the savage creature,
or who would venture to set him free. I slipped in in safety and
lay awake half the night in my joy at the thought of seeing you.
I had no difficulty in getting leave to come into Winchester this
morning, but I must be back before three o'clock, for Mr. and
Mrs. Rucastle are going on a visit, and will be away all the
evening, so that I must look after the child. Now I have told you
all my adventures, Mr. Holmes, and I should be very glad if you
could tell me what it all means, and, above all, what I should
do."
Holmes and I had listened spellbound to this extraordinary story.
My friend rose now and paced up and down the room, his hands in
his pockets, and an expression of the most profound gravity upon
his face.
"Is Toller still drunk?" he asked.
"Yes. I heard his wife tell Mrs. Rucastle that she could do
nothing with him."
"That is well. And the Rucastles go out to-night?"
"Yes."
"Is there a cellar with a good strong lock?"
"Yes, the wine-cellar."
"You seem to me to have acted all through this matter like a very
brave and sensible girl, Miss Hunter. Do you think that you could
perform one more feat? I should not ask it of you if I did not
think you a quite exceptional woman."
"I will try. What is it?"
"We shall be at the Copper Beeches by seven o'clock, my friend
and I. The Rucastles will be gone by that time, and Toller will,
we hope, be incapable. There only remains Mrs. Toller, who might
give the alarm. If you could send her into the cellar on some
errand, and then turn the key upon her, you would facilitate
matters immensely."
"I will do it."
"Excellent! We shall then look thoroughly into the affair. Of
course there is only one feasible explanation. You have been
brought there to personate someone, and the real person is
imprisoned in this chamber. That is obvious. As to who this
prisoner is, I have no doubt that it is the daughter, Miss Alice
Rucastle, if I remember right, who was said to have gone to
America. You were chosen, doubtless, as resembling her in height,
figure, and the colour of your hair. Hers had been cut off, very
possibly in some illness through which she has passed, and so, of
course, yours had to be sacrificed also. By a curious chance you
came upon her tresses. The man in the road was undoubtedly some
friend of hers--possibly her fiancé--and no doubt, as you wore
the girl's dress and were so like her, he was convinced from your
laughter, whenever he saw you, and afterwards from your gesture,
that Miss Rucastle was perfectly happy, and that she no longer
desired his attentions. The dog is let loose at night to prevent
him from endeavouring to communicate with her. So much is fairly
clear. The most serious point in the case is the disposition of
the child."
"What on earth has that to do with it?" I ejaculated.
"My dear Watson, you as a medical man are continually gaining
light as to the tendencies of a child by the study of the
parents. Don't you see that the converse is equally valid. I have
frequently gained my first real insight into the character of
parents by studying their children. This child's disposition is
abnormally cruel, merely for cruelty's sake, and whether he
derives this from his smiling father, as I should suspect, or
from his mother, it bodes evil for the poor girl who is in their
power."
"I am sure that you are right, Mr. Holmes," cried our client. "A
thousand things come back to me which make me certain that you
have hit it. Oh, let us lose not an instant in bringing help to
this poor creature."
"We must be circumspect, for we are dealing with a very cunning
man. We can do nothing until seven o'clock. At that hour we shall
be with you, and it will not be long before we solve the
mystery."
We were as good as our word, for it was just seven when we
reached the Copper Beeches, having put up our trap at a wayside
public-house. The group of trees, with their dark leaves shining
like burnished metal in the light of the setting sun, were
sufficient to mark the house even had Miss Hunter not been
standing smiling on the door-step.
"Have you managed it?" asked Holmes.
A loud thudding noise came from somewhere downstairs. "That is
Mrs. Toller in the cellar," said she. "Her husband lies snoring
on the kitchen rug. Here are his keys, which are the duplicates
of Mr. Rucastle's."
"You have done well indeed!" cried Holmes with enthusiasm. "Now
lead the way, and we shall soon see the end of this black
business."
We passed up the stair, unlocked the door, followed on down a
passage, and found ourselves in front of the barricade which Miss
Hunter had described. Holmes cut the cord and removed the
transverse bar. Then he tried the various keys in the lock, but
without success. No sound came from within, and at the silence
Holmes' face clouded over.
"I trust that we are not too late," said he. "I think, Miss
Hunter, that we had better go in without you. Now, Watson, put
your shoulder to it, and we shall see whether we cannot make our
way in."
It was an old rickety door and gave at once before our united
strength. Together we rushed into the room. It was empty. There
was no furniture save a little pallet bed, a small table, and a
basketful of linen. The skylight above was open, and the prisoner
gone.
"There has been some villainy here," said Holmes; "this beauty
has guessed Miss Hunter's intentions and has carried his victim
off."
"But how?"
"Through the skylight. We shall soon see how he managed it." He
swung himself up onto the roof. "Ah, yes," he cried, "here's the
end of a long light ladder against the eaves. That is how he did
it."
"But it is impossible," said Miss Hunter; "the ladder was not
there when the Rucastles went away."
"He has come back and done it. I tell you that he is a clever and
dangerous man. I should not be very much surprised if this were
he whose step I hear now upon the stair. I think, Watson, that it
would be as well for you to have your pistol ready."
The words were hardly out of his mouth before a man appeared at
the door of the room, a very fat and burly man, with a heavy
stick in his hand. Miss Hunter screamed and shrunk against the
wall at the sight of him, but Sherlock Holmes sprang forward and
confronted him.
"You villain!" said he, "where's your daughter?"
The fat man cast his eyes round, and then up at the open
skylight.
"It is for me to ask you that," he shrieked, "you thieves! Spies
and thieves! I have caught you, have I? You are in my power. I'll
serve you!" He turned and clattered down the stairs as hard as he
could go.
"He's gone for the dog!" cried Miss Hunter.
"I have my revolver," said I.
"Better close the front door," cried Holmes, and we all rushed
down the stairs together. We had hardly reached the hall when we
heard the baying of a hound, and then a scream of agony, with a
horrible worrying sound which it was dreadful to listen to. An
elderly man with a red face and shaking limbs came staggering out
at a side door.
"My God!" he cried. "Someone has loosed the dog. It's not been
fed for two days. Quick, quick, or it'll be too late!"
Holmes and I rushed out and round the angle of the house, with
Toller hurrying behind us. There was the huge famished brute, its
black muzzle buried in Rucastle's throat, while he writhed and
screamed upon the ground. Running up, I blew its brains out, and
it fell over with its keen white teeth still meeting in the great
creases of his neck. With much labour we separated them and
carried him, living but horribly mangled, into the house. We laid
him upon the drawing-room sofa, and having dispatched the sobered
Toller to bear the news to his wife, I did what I could to
relieve his pain. We were all assembled round him when the door
opened, and a tall, gaunt woman entered the room.
"Mrs. Toller!" cried Miss Hunter.
"Yes, miss. Mr. Rucastle let me out when he came back before he
went up to you. Ah, miss, it is a pity you didn't let me know
what you were planning, for I would have told you that your pains
were wasted."
"Ha!" said Holmes, looking keenly at her. "It is clear that Mrs.
Toller knows more about this matter than anyone else."
"Yes, sir, I do, and I am ready enough to tell what I know."
"Then, pray, sit down, and let us hear it for there are several
points on which I must confess that I am still in the dark."
"I will soon make it clear to you," said she; "and I'd have done
so before now if I could ha' got out from the cellar. If there's
police-court business over this, you'll remember that I was the
one that stood your friend, and that I was Miss Alice's friend
too.
"She was never happy at home, Miss Alice wasn't, from the time
that her father married again. She was slighted like and had no
say in anything, but it never really became bad for her until
after she met Mr. Fowler at a friend's house. As well as I could
learn, Miss Alice had rights of her own by will, but she was so
quiet and patient, she was, that she never said a word about them
but just left everything in Mr. Rucastle's hands. He knew he was
safe with her; but when there was a chance of a husband coming
forward, who would ask for all that the law would give him, then
her father thought it time to put a stop on it. He wanted her to
sign a paper, so that whether she married or not, he could use
her money. When she wouldn't do it, he kept on worrying her until
she got brain-fever, and for six weeks was at death's door. Then
she got better at last, all worn to a shadow, and with her
beautiful hair cut off; but that didn't make no change in her
young man, and he stuck to her as true as man could be."
"Ah," said Holmes, "I think that what you have been good enough
to tell us makes the matter fairly clear, and that I can deduce
all that remains. Mr. Rucastle then, I presume, took to this
system of imprisonment?"
"Yes, sir."
"And brought Miss Hunter down from London in order to get rid of
the disagreeable persistence of Mr. Fowler."
"That was it, sir."
"But Mr. Fowler being a persevering man, as a good seaman should
be, blockaded the house, and having met you succeeded by certain
arguments, metallic or otherwise, in convincing you that your
interests were the same as his."
"Mr. Fowler was a very kind-spoken, free-handed gentleman," said
Mrs. Toller serenely.
"And in this way he managed that your good man should have no
want of drink, and that a ladder should be ready at the moment
when your master had gone out."
"You have it, sir, just as it happened."
"I am sure we owe you an apology, Mrs. Toller," said Holmes, "for
you have certainly cleared up everything which puzzled us. And
here comes the country surgeon and Mrs. Rucastle, so I think,
Watson, that we had best escort Miss Hunter back to Winchester,
as it seems to me that our locus standi now is rather a
questionable one."
And thus was solved the mystery of the sinister house with the
copper beeches in front of the door. Mr. Rucastle survived, but
was always a broken man, kept alive solely through the care of
his devoted wife. They still live with their old servants, who
probably know so much of Rucastle's past life that he finds it
difficult to part from them. Mr. Fowler and Miss Rucastle were
married, by special license, in Southampton the day after their
flight, and he is now the holder of a government appointment in
the island of Mauritius. As to Miss Violet Hunter, my friend
Holmes, rather to my disappointment, manifested no further
interest in her when once she had ceased to be the centre of one
of his problems, and she is now the head of a private school at
Walsall, where I believe that she has met with considerable success.
End of the Project Gutenberg EBook of The Adventures of Sherlock Holmes, by
Arthur Conan Doyle
*** END OF THIS PROJECT GUTENBERG EBOOK THE ADVENTURES OF SHERLOCK HOLMES ***
***** This file should be named 1661-8.txt or 1661-8.zip *****
This and all associated files of various formats will be found in:
http://www.gutenberg.org/1/6/6/1661/
Produced by an anonymous Project Gutenberg volunteer and Jose Menendez
Updated editions will replace the previous one--the old editions
will be renamed.
Creating the works from public domain print editions means that no
one owns a United States copyright in these works, so the Foundation
(and you!) can copy and distribute it in the United States without
permission and without paying copyright royalties. Special rules,
set forth in the General Terms of Use part of this license, apply to
copying and distributing Project Gutenberg-tm electronic works to
protect the PROJECT GUTENBERG-tm concept and trademark. Project
Gutenberg is a registered trademark, and may not be used if you
charge for the eBooks, unless you receive specific permission. If you
do not charge anything for copies of this eBook, complying with the
rules is very easy. You may use this eBook for nearly any purpose
such as creation of derivative works, reports, performances and
research. They may be modified and printed and given away--you may do
practically ANYTHING with public domain eBooks. Redistribution is
subject to the trademark license, especially commercial
redistribution.
*** START: FULL LICENSE ***
THE FULL PROJECT GUTENBERG LICENSE
PLEASE READ THIS BEFORE YOU DISTRIBUTE OR USE THIS WORK
To protect the Project Gutenberg-tm mission of promoting the free
distribution of electronic works, by using or distributing this work
(or any other work associated in any way with the phrase "Project
Gutenberg"), you agree to comply with all the terms of the Full Project
Gutenberg-tm License (available with this file or online at
http://gutenberg.net/license).
Section 1. General Terms of Use and Redistributing Project Gutenberg-tm
electronic works
1.A. By reading or using any part of this Project Gutenberg-tm
electronic work, you indicate that you have read, understand, agree to
and accept all the terms of this license and intellectual property
(trademark/copyright) agreement. If you do not agree to abide by all
the terms of this agreement, you must cease using and return or destroy
all copies of Project Gutenberg-tm electronic works in your possession.
If you paid a fee for obtaining a copy of or access to a Project
Gutenberg-tm electronic work and you do not agree to be bound by the
terms of this agreement, you may obtain a refund from the person or
entity to whom you paid the fee as set forth in paragraph 1.E.8.
1.B. "Project Gutenberg" is a registered trademark. It may only be
used on or associated in any way with an electronic work by people who
agree to be bound by the terms of this agreement. There are a few
things that you can do with most Project Gutenberg-tm electronic works
even without complying with the full terms of this agreement. See
paragraph 1.C below. There are a lot of things you can do with Project
Gutenberg-tm electronic works if you follow the terms of this agreement
and help preserve free future access to Project Gutenberg-tm electronic
works. See paragraph 1.E below.
1.C. The Project Gutenberg Literary Archive Foundation ("the Foundation"
or PGLAF), owns a compilation copyright in the collection of Project
Gutenberg-tm electronic works. Nearly all the individual works in the
collection are in the public domain in the United States. If an
individual work is in the public domain in the United States and you are
located in the United States, we do not claim a right to prevent you from
copying, distributing, performing, displaying or creating derivative
works based on the work as long as all references to Project Gutenberg
are removed. Of course, we hope that you will support the Project
Gutenberg-tm mission of promoting free access to electronic works by
freely sharing Project Gutenberg-tm works in compliance with the terms of
this agreement for keeping the Project Gutenberg-tm name associated with
the work. You can easily comply with the terms of this agreement by
keeping this work in the same format with its attached full Project
Gutenberg-tm License when you share it without charge with others.
1.D. The copyright laws of the place where you are located also govern
what you can do with this work. Copyright laws in most countries are in
a constant state of change. If you are outside the United States, check
the laws of your country in addition to the terms of this agreement
before downloading, copying, displaying, performing, distributing or
creating derivative works based on this work or any other Project
Gutenberg-tm work. The Foundation makes no representations concerning
the copyright status of any work in any country outside the United
States.
1.E. Unless you have removed all references to Project Gutenberg:
1.E.1. The following sentence, with active links to, or other immediate
access to, the full Project Gutenberg-tm License must appear prominently
whenever any copy of a Project Gutenberg-tm work (any work on which the
phrase "Project Gutenberg" appears, or with which the phrase "Project
Gutenberg" is associated) is accessed, displayed, performed, viewed,
copied or distributed:
This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever. You may copy it, give it away or
re-use it under the terms of the Project Gutenberg License included
with this eBook or online at www.gutenberg.net
1.E.2. If an individual Project Gutenberg-tm electronic work is derived
from the public domain (does not contain a notice indicating that it is
posted with permission of the copyright holder), the work can be copied
and distributed to anyone in the United States without paying any fees
or charges. If you are redistributing or providing access to a work
with the phrase "Project Gutenberg" associated with or appearing on the
work, you must comply either with the requirements of paragraphs 1.E.1
through 1.E.7 or obtain permission for the use of the work and the
Project Gutenberg-tm trademark as set forth in paragraphs 1.E.8 or
1.E.9.
1.E.3. If an individual Project Gutenberg-tm electronic work is posted
with the permission of the copyright holder, your use and distribution
must comply with both paragraphs 1.E.1 through 1.E.7 and any additional
terms imposed by the copyright holder. Additional terms will be linked
to the Project Gutenberg-tm License for all works posted with the
permission of the copyright holder found at the beginning of this work.
1.E.4. Do not unlink or detach or remove the full Project Gutenberg-tm
License terms from this work, or any files containing a part of this
work or any other work associated with Project Gutenberg-tm.
1.E.5. Do not copy, display, perform, distribute or redistribute this
electronic work, or any part of this electronic work, without
prominently displaying the sentence set forth in paragraph 1.E.1 with
active links or immediate access to the full terms of the Project
Gutenberg-tm License.
1.E.6. You may convert to and distribute this work in any binary,
compressed, marked up, nonproprietary or proprietary form, including any
word processing or hypertext form. However, if you provide access to or
distribute copies of a Project Gutenberg-tm work in a format other than
"Plain Vanilla ASCII" or other format used in the official version
posted on the official Project Gutenberg-tm web site (www.gutenberg.net),
you must, at no additional cost, fee or expense to the user, provide a
copy, a means of exporting a copy, or a means of obtaining a copy upon
request, of the work in its original "Plain Vanilla ASCII" or other
form. Any alternate format must include the full Project Gutenberg-tm
License as specified in paragraph 1.E.1.
1.E.7. Do not charge a fee for access to, viewing, displaying,
performing, copying or distributing any Project Gutenberg-tm works
unless you comply with paragraph 1.E.8 or 1.E.9.
1.E.8. You may charge a reasonable fee for copies of or providing
access to or distributing Project Gutenberg-tm electronic works provided
that
- You pay a royalty fee of 20% of the gross profits you derive from
the use of Project Gutenberg-tm works calculated using the method
you already use to calculate your applicable taxes. The fee is
owed to the owner of the Project Gutenberg-tm trademark, but he
has agreed to donate royalties under this paragraph to the
Project Gutenberg Literary Archive Foundation. Royalty payments
must be paid within 60 days following each date on which you
prepare (or are legally required to prepare) your periodic tax
returns. Royalty payments should be clearly marked as such and
sent to the Project Gutenberg Literary Archive Foundation at the
address specified in Section 4, "Information about donations to
the Project Gutenberg Literary Archive Foundation."
- You provide a full refund of any money paid by a user who notifies
you in writing (or by e-mail) within 30 days of receipt that s/he
does not agree to the terms of the full Project Gutenberg-tm
License. You must require such a user to return or
destroy all copies of the works possessed in a physical medium
and discontinue all use of and all access to other copies of
Project Gutenberg-tm works.
- You provide, in accordance with paragraph 1.F.3, a full refund of any
money paid for a work or a replacement copy, if a defect in the
electronic work is discovered and reported to you within 90 days
of receipt of the work.
- You comply with all other terms of this agreement for free
distribution of Project Gutenberg-tm works.
1.E.9. If you wish to charge a fee or distribute a Project Gutenberg-tm
electronic work or group of works on different terms than are set
forth in this agreement, you must obtain permission in writing from
both the Project Gutenberg Literary Archive Foundation and Michael
Hart, the owner of the Project Gutenberg-tm trademark. Contact the
Foundation as set forth in Section 3 below.
1.F.
1.F.1. Project Gutenberg volunteers and employees expend considerable
effort to identify, do copyright research on, transcribe and proofread
public domain works in creating the Project Gutenberg-tm
collection. Despite these efforts, Project Gutenberg-tm electronic
works, and the medium on which they may be stored, may contain
"Defects," such as, but not limited to, incomplete, inaccurate or
corrupt data, transcription errors, a copyright or other intellectual
property infringement, a defective or damaged disk or other medium, a
computer virus, or computer codes that damage or cannot be read by
your equipment.
1.F.2. LIMITED WARRANTY, DISCLAIMER OF DAMAGES - Except for the "Right
of Replacement or Refund" described in paragraph 1.F.3, the Project
Gutenberg Literary Archive Foundation, the owner of the Project
Gutenberg-tm trademark, and any other party distributing a Project
Gutenberg-tm electronic work under this agreement, disclaim all
liability to you for damages, costs and expenses, including legal
fees. YOU AGREE THAT YOU HAVE NO REMEDIES FOR NEGLIGENCE, STRICT
LIABILITY, BREACH OF WARRANTY OR BREACH OF CONTRACT EXCEPT THOSE
PROVIDED IN PARAGRAPH 1.F.3. YOU AGREE THAT THE FOUNDATION, THE
TRADEMARK OWNER, AND ANY DISTRIBUTOR UNDER THIS AGREEMENT WILL NOT BE
LIABLE TO YOU FOR ACTUAL, DIRECT, INDIRECT, CONSEQUENTIAL, PUNITIVE OR
INCIDENTAL DAMAGES EVEN IF YOU GIVE NOTICE OF THE POSSIBILITY OF SUCH
DAMAGE.
1.F.3. LIMITED RIGHT OF REPLACEMENT OR REFUND - If you discover a
defect in this electronic work within 90 days of receiving it, you can
receive a refund of the money (if any) you paid for it by sending a
written explanation to the person you received the work from. If you
received the work on a physical medium, you must return the medium with
your written explanation. The person or entity that provided you with
the defective work may elect to provide a replacement copy in lieu of a
refund. If you received the work electronically, the person or entity
providing it to you may choose to give you a second opportunity to
receive the work electronically in lieu of a refund. If the second copy
is also defective, you may demand a refund in writing without further
opportunities to fix the problem.
1.F.4. Except for the limited right of replacement or refund set forth
in paragraph 1.F.3, this work is provided to you 'AS-IS' WITH NO OTHER
WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
WARRANTIES OF MERCHANTIBILITY OR FITNESS FOR ANY PURPOSE.
1.F.5. Some states do not allow disclaimers of certain implied
warranties or the exclusion or limitation of certain types of damages.
If any disclaimer or limitation set forth in this agreement violates the
law of the state applicable to this agreement, the agreement shall be
interpreted to make the maximum disclaimer or limitation permitted by
the applicable state law. The invalidity or unenforceability of any
provision of this agreement shall not void the remaining provisions.
1.F.6. INDEMNITY - You agree to indemnify and hold the Foundation, the
trademark owner, any agent or employee of the Foundation, anyone
providing copies of Project Gutenberg-tm electronic works in accordance
with this agreement, and any volunteers associated with the production,
promotion and distribution of Project Gutenberg-tm electronic works,
harmless from all liability, costs and expenses, including legal fees,
that arise directly or indirectly from any of the following which you do
or cause to occur: (a) distribution of this or any Project Gutenberg-tm
work, (b) alteration, modification, or additions or deletions to any
Project Gutenberg-tm work, and (c) any Defect you cause.
Section 2. Information about the Mission of Project Gutenberg-tm
Project Gutenberg-tm is synonymous with the free distribution of
electronic works in formats readable by the widest variety of computers
including obsolete, old, middle-aged and new computers. It exists
because of the efforts of hundreds of volunteers and donations from
people in all walks of life.
Volunteers and financial support to provide volunteers with the
assistance they need are critical to reaching Project Gutenberg-tm's
goals and ensuring that the Project Gutenberg-tm collection will
remain freely available for generations to come. In 2001, the Project
Gutenberg Literary Archive Foundation was created to provide a secure
and permanent future for Project Gutenberg-tm and future generations.
To learn more about the Project Gutenberg Literary Archive Foundation
and how your efforts and donations can help, see Sections 3 and 4
and the Foundation web page at http://www.pglaf.org.
Section 3. Information about the Project Gutenberg Literary Archive
Foundation
The Project Gutenberg Literary Archive Foundation is a non profit
501(c)(3) educational corporation organized under the laws of the
state of Mississippi and granted tax exempt status by the Internal
Revenue Service. The Foundation's EIN or federal tax identification
number is 64-6221541. Its 501(c)(3) letter is posted at
http://pglaf.org/fundraising. Contributions to the Project Gutenberg
Literary Archive Foundation are tax deductible to the full extent
permitted by U.S. federal laws and your state's laws.
The Foundation's principal office is located at 4557 Melan Dr. S.
Fairbanks, AK, 99712., but its volunteers and employees are scattered
throughout numerous locations. Its business office is located at
809 North 1500 West, Salt Lake City, UT 84116, (801) 596-1887, email
business@pglaf.org. Email contact links and up to date contact
information can be found at the Foundation's web site and official
page at http://pglaf.org
For additional contact information:
Dr. Gregory B. Newby
Chief Executive and Director
gbnewby@pglaf.org
Section 4. Information about Donations to the Project Gutenberg
Literary Archive Foundation
Project Gutenberg-tm depends upon and cannot survive without wide
spread public support and donations to carry out its mission of
increasing the number of public domain and licensed works that can be
freely distributed in machine readable form accessible by the widest
array of equipment including outdated equipment. Many small donations
($1 to $5,000) are particularly important to maintaining tax exempt
status with the IRS.
The Foundation is committed to complying with the laws regulating
charities and charitable donations in all 50 states of the United
States. Compliance requirements are not uniform and it takes a
considerable effort, much paperwork and many fees to meet and keep up
with these requirements. We do not solicit donations in locations
where we have not received written confirmation of compliance. To
SEND DONATIONS or determine the status of compliance for any
particular state visit http://pglaf.org
While we cannot and do not solicit contributions from states where we
have not met the solicitation requirements, we know of no prohibition
against accepting unsolicited donations from donors in such states who
approach us with offers to donate.
International donations are gratefully accepted, but we cannot make
any statements concerning tax treatment of donations received from
outside the United States. U.S. laws alone swamp our small staff.
Please check the Project Gutenberg Web pages for current donation
methods and addresses. Donations are accepted in a number of other
ways including including checks, online payments and credit card
donations. To donate, please visit: http://pglaf.org/donate
Section 5. General Information About Project Gutenberg-tm electronic
works.
Professor Michael S. Hart is the originator of the Project Gutenberg-tm
concept of a library of electronic works that could be freely shared
with anyone. For thirty years, he produced and distributed Project
Gutenberg-tm eBooks with only a loose network of volunteer support.
Project Gutenberg-tm eBooks are often created from several printed
editions, all of which are confirmed as Public Domain in the U.S.
unless a copyright notice is included. Thus, we do not necessarily
keep eBooks in compliance with any particular paper edition.
Most people start at our Web site which has the main PG search facility:
http://www.gutenberg.net
This Web site includes information about Project Gutenberg-tm,
including how to make donations to the Project Gutenberg Literary
Archive Foundation, how to help produce our new eBooks, and how to
subscribe to our email newsletter to hear about new eBooks.
================================================
FILE: packages/interface-ipfs-core/test/fixtures/hidden-files-folder/ipfs-add.js
================================================
#!/usr/bin/env node
const ipfs = require('../src')('localhost', 5001)
const files = process.argv.slice(2)
ipfs.add(files, { recursive: true }, function (err, res) {
if (err || !res) return console.log(err)
for (let i = 0; i < res.length; i++) {
console.log('added', res[i].Hash, res[i].Name)
}
})
================================================
FILE: packages/interface-ipfs-core/test/fixtures/hidden-files-folder/jungle.txt
================================================
Mowgli's Brothers
Now Rann the Kite brings home the night
That Mang the Bat sets free--
The herds are shut in byre and hut
For loosed till dawn are we.
This is the hour of pride and power,
Talon and tush and claw.
Oh, hear the call!--Good hunting all
That keep the Jungle Law!
Night-Song in the Jungle
It was seven o'clock of a very warm evening in the Seeonee hills when
Father Wolf woke up from his day's rest, scratched himself, yawned, and
spread out his paws one after the other to get rid of the sleepy feeling
in their tips. Mother Wolf lay with her big gray nose dropped across her
four tumbling, squealing cubs, and the moon shone into the mouth of the
cave where they all lived. "Augrh!" said Father Wolf. "It is time to
hunt again." He was going to spring down hill when a little shadow with
a bushy tail crossed the threshold and whined: "Good luck go with you, O
Chief of the Wolves. And good luck and strong white teeth go with noble
children that they may never forget the hungry in this world."
It was the jackal--Tabaqui, the Dish-licker--and the wolves of India
despise Tabaqui because he runs about making mischief, and telling
tales, and eating rags and pieces of leather from the village
rubbish-heaps. But they are afraid of him too, because Tabaqui, more
than anyone else in the jungle, is apt to go mad, and then he forgets
that he was ever afraid of anyone, and runs through the forest biting
everything in his way. Even the tiger runs and hides when little Tabaqui
goes mad, for madness is the most disgraceful thing that can overtake
a wild creature. We call it hydrophobia, but they call it dewanee--the
madness--and run.
"Enter, then, and look," said Father Wolf stiffly, "but there is no food
here."
"For a wolf, no," said Tabaqui, "but for so mean a person as myself a
dry bone is a good feast. Who are we, the Gidur-log [the jackal people],
to pick and choose?" He scuttled to the back of the cave, where he
found the bone of a buck with some meat on it, and sat cracking the end
merrily.
"All thanks for this good meal," he said, licking his lips. "How
beautiful are the noble children! How large are their eyes! And so young
too! Indeed, indeed, I might have remembered that the children of kings
================================================
FILE: packages/interface-ipfs-core/test/fixtures/hidden-files-folder/pp.txt
================================================
PRIDE AND PREJUDICE
By Jane Austen
Chapter 1
It is a truth universally acknowledged, that a single man in possession
of a good fortune, must be in want of a wife.
However little known the feelings or views of such a man may be on his
first entering a neighbourhood, this truth is so well fixed in the minds
of the surrounding families, that he is considered the rightful property
of some one or other of their daughters.
"My dear Mr. Bennet," said his lady to him one day, "have you heard that
Netherfield Park is let at last?"
Mr. Bennet replied that he had not.
"But it is," returned she; "for Mrs. Long has just been here, and she
told me all about it."
Mr. Bennet made no answer.
"Do you not want to know who has taken it?" cried his wife impatiently.
"_You_ want to tell me, and I have no objection to hearing it."
This was invitation enough.
"Why, my dear, you must know, Mrs. Long says that Netherfield is taken
by a young man of large fortune from the north of England; that he came
down on Monday in a chaise and four to see the place, and was so much
delighted with it, that he agreed with Mr. Morris immediately; that he
is to take possession before Michaelmas, and some of his servants are to
be in the house by the end of next week."
"What is his name?"
"Bingley."
"Is he married or single?"
"Oh! Single, my dear, to be sure! A single man of large fortune; four or
five thousand a year. What a fine thing for our girls!"
"How so? How can it affect them?"
"My dear Mr. Bennet," replied his wife, "how can you be so tiresome! You
must know that I am thinking of his marrying one of them."
"Is that his design in settling here?"
"Design! Nonsense, how can you talk so! But it is very likely that he
_may_ fall in love with one of them, and therefore you must visit him as
soon as he comes."
"I see no occasion for that. You and the girls may go, or you may send
them by themselves, which perhaps will be still better, for as you are
as handsome as any of them, Mr. Bingley may like you the best of the
party."
"My dear, you flatter me. I certainly _have_ had my share of beauty, but
I do not pretend to be anything extraordinary now. When a woman has five
grown-up daughters, she ought to give over thinking of her own beauty."
"In such cases, a woman has not often much beauty to think of."
"But, my dear, you must indeed go and see Mr. Bingley when he comes into
the neighbourhood."
"It is more than I engage for, I assure you."
"But consider your daughters. Only think what an establishment it would
be for one of them. Sir William and Lady Lucas are determined to
go, merely on that account, for in general, you know, they visit no
newcomers. Indeed you must go, for it will be impossible for _us_ to
visit him if you do not."
"You are over-scrupulous, surely. I dare say Mr. Bingley will be very
glad to see you; and I will send a few lines by you to assure him of my
hearty consent to his marrying whichever he chooses of the girls; though
I must throw in a good word for my little Lizzy."
"I desire you will do no such thing. Lizzy is not a bit better than the
others; and I am sure she is not half so handsome as Jane, nor half so
good-humoured as Lydia. But you are always giving _her_ the preference."
"They have none of them much to recommend them," replied he; "they are
all silly and ignorant like other girls; but Lizzy has something more of
quickness than her sisters."
"Mr. Bennet, how _can_ you abuse your own children in such a way? You
take delight in vexing me. You have no compassion for my poor nerves."
"You mistake me, my dear. I have a high respect for your nerves. They
are my old friends. I have heard you mention them with consideration
these last twenty years at least."
"Ah, you do not know what I suffer."
"But I hope you will get over it, and live to see many young men of four
thousand a year come into the neighbourhood."
"It will be no use to us, if twenty such should come, since you will not
visit them."
"Depend upon it, my dear, that when there are twenty, I will visit them
all."
Mr. Bennet was so odd a mixture of quick parts, sarcastic humour,
reserve, and caprice, that the experience of three-and-twenty years had
been insufficient to make his wife understand his character. _Her_ mind
was less difficult to develop. She was a woman of mean understanding,
little information, and uncertain temper. When she was discontented,
she fancied herself nervous. The business of her life was to get her
daughters married; its solace was visiting and news.
================================================
FILE: packages/interface-ipfs-core/test/fixtures/refs-test/animals/land/african.txt
================================================
elephant
rhinocerous
================================================
FILE: packages/interface-ipfs-core/test/fixtures/refs-test/animals/land/americas.txt
================================================
ñandu
tapir
================================================
FILE: packages/interface-ipfs-core/test/fixtures/refs-test/animals/land/australian.txt
================================================
emu
kangaroo
================================================
FILE: packages/interface-ipfs-core/test/fixtures/refs-test/animals/sea/atlantic.txt
================================================
dolphin
whale
================================================
FILE: packages/interface-ipfs-core/test/fixtures/refs-test/animals/sea/indian.txt
================================================
cuttlefish
octopus
================================================
FILE: packages/interface-ipfs-core/test/fixtures/refs-test/atlantic-animals
================================================
dolphin
whale
================================================
FILE: packages/interface-ipfs-core/test/fixtures/refs-test/fruits/tropical.txt
================================================
banana
pineapple
================================================
FILE: packages/interface-ipfs-core/test/fixtures/refs-test/mushroom.txt
================================================
mushroom
================================================
FILE: packages/interface-ipfs-core/test/fixtures/ssl/cert.pem
================================================
-----BEGIN CERTIFICATE-----
MIIC8jCCAdqgAwIBAgIUA7b/br1Ovf/mNDhm3P26ewfHbpIwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE5MDEyNDE4MjE0OVoYDzIxMTkw
MTI0MTgyMTQ5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCuskZ+qCJz72ihFgrF6yvjy7pXpETtHXJK+elXoU6M
oWNukWOgk9EZQow43wM6pQR/rTxDWE9W/qSpLb08cvW03+RlbyQn0lkO327rN9Nd
VjP1Nu6WwAdk6U0CaGdNe4dwxc69eB3ZS4B32d/GIpIti23F3bRxAKE15km+Ufhj
X1NGuFqbJOYHfbqOMkgMlkO54y4gAJa5tQnb3n0pNzpIklSzBO65T/u1HAsVDVZW
BgVW/pqourvh6TtjCA3LJp33T9IcItTAlYbiFM0hKytlePzaHG6OKRazO6Z46l/V
gTvRY90B7LrQWnSY3saLYuv9Gs5qQvEvthP/U6LroXjNAgMBAAGjOjA4MBQGA1Ud
EQQNMAuCCWxvY2FsaG9zdDALBgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
AwEwDQYJKoZIhvcNAQELBQADggEBAJHi6CJk5aZViLK+dm/ruV2vBiqGuRgfuviJ
Mb+iApO39Q/PjxE2IQoVVcf7Rpml2SSARyN7K9cxLdSFFZn3Wgq3yHXB6vhsyGO+
r17awBEI08PUlCuYVlE/mEzHGUGYbR0whIQSWK+gLMSQ2NG11DJyIPnErZYM1XSS
p9ERjyR4KXC33RxEc0AtGZsGgCThGkmwas3v702pzGfDd3qpbXztb+jdbfUVMUj4
Wrzhps9JZ6HJ8RZBjnSMMqmWDbvJI+2aG0Ky6BYChrARLn9H7rCMgfe0l0QIL5br
T1BqL+HHCVNiyt82+byg5mjpcsKvojCrQVVcQBs1xUkk6F45LeU=
-----END CERTIFICATE-----
================================================
FILE: packages/interface-ipfs-core/test/fixtures/ssl/privkey.pem
================================================
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCuskZ+qCJz72ih
FgrF6yvjy7pXpETtHXJK+elXoU6MoWNukWOgk9EZQow43wM6pQR/rTxDWE9W/qSp
Lb08cvW03+RlbyQn0lkO327rN9NdVjP1Nu6WwAdk6U0CaGdNe4dwxc69eB3ZS4B3
2d/GIpIti23F3bRxAKE15km+UfhjX1NGuFqbJOYHfbqOMkgMlkO54y4gAJa5tQnb
3n0pNzpIklSzBO65T/u1HAsVDVZWBgVW/pqourvh6TtjCA3LJp33T9IcItTAlYbi
FM0hKytlePzaHG6OKRazO6Z46l/VgTvRY90B7LrQWnSY3saLYuv9Gs5qQvEvthP/
U6LroXjNAgMBAAECggEAYukJRNkJeL7KbLpAK0M1vGoy/UBCzkXn2k+ZMEZiZPlT
hNzInbhToYuuPNz3xRJ9c5SwFClB8q2GqUr+Y+Vq/JfvhwbgX7OXPPaApKkdATG3
hVUuzSe4iAgX1A8svg/85Xr5zQjfTZKUEEfJjTMxtJvG8UrPyVNj81KJ2joq+oek
CspuQrhP9mpUaPBGmBykP0qbPqRO/E/eqMMEyfURDNXQjH1i4TLzUsIjd5IdkA9h
DoC4fzK6R6LTjRdIEXK2xJGSKgIDnNu7sC72tTLRrbd4NeMjWg/yWEd7W/qQIy/6
JG1Zj8zgjK1UnkPxZIwGuSOgCcNSCjA0WiSkF6sGeQKBgQDmvLIqCDaNDt+EOLoM
HqXyoedriaJwzyTVUPf+EFHwwrOo/HoKesfca5coNIEJHMp5DjS+syPVTwKOfK82
ipPT9UuW+F9N+dWGKjCiZ0vwc6ijrq2pEeXuxrJWsIJ3yoKTXZWTRbRnu6kA+dVJ
XGwqwnHni8cikAhdMzGyGi0vXwKBgQDB0tIfGOwmHo+0W94KjnIoB8dA/Cy9K0V+
l/9IBX8I5/hFV2BF4IZoWepJ15UAJR855Tw5+0ea+/QpdSrl9MmmUQ0Hftvlmypp
XYIDbWewgKHZOO6TnCs6FLtm6Zrq/1yrppnTslLXq/4QkOPSbQ9uY83N3upxfRIb
V9//OLMDUwKBgHk7d9kBy7e9ss8EByzLBaJAUxl7jW/8RnwWONayuHrpsf/9+Bl9
fXlgxmEHhSzGhdOpFSmFcjRneQ5okJ71nMpnPboq8dhEhl4h2L/byliiTF8ELpaA
ovEcUSOfRk2uh4DqUOa6XxmJzjiHC/uppeOpmrNwC8crKlndxiSwAEG9AoGAFNLS
kla6IEpORCFOlLHDH/vd82RkZhp9B+HKonE8ubc6XDDL/hXmOtXWLwLDVlWmqjCv
rMcLZWJGVCHrbvNCquSwUqrVczCdeN579mRNrI/VU6IjN6aimkXZ8G+Onkq7KRHo
Gu9gqR0oWZ1HbLcc3k5IsSKO64x1YoypWyE7UlMCgYBaMIyJb8hd+lcgTy+WNJwZ
ovcp8KoxUFONA/fd3iyWKYPq+5OTD5iDOy9LKXUrlYduFEQog6vUamlQyoyTplLs
d3sIeeu9RRvuCidKzHtmQNAOw8adIa4GAnBvJ2G2oZ3lWnDgIbKgHido+YSyyI2E
jZOVuk3817sAOTaWSlQEZQ==
-----END PRIVATE KEY-----
================================================
FILE: packages/interface-ipfs-core/test/fixtures/test-folder/alice.txt
================================================
CHAPTER XII. Alice's Evidence
'Here!' cried Alice, quite forgetting in the flurry of the moment how
large she had grown in the last few minutes, and she jumped up in such
a hurry that she tipped over the jury-box with the edge of her skirt,
upsetting all the jurymen on to the heads of the crowd below, and there
they lay sprawling about, reminding her very much of a globe of goldfish
she had accidentally upset the week before.
'Oh, I BEG your pardon!' she exclaimed in a tone of great dismay, and
began picking them up again as quickly as she could, for the accident of
the goldfish kept running in her head, and she had a vague sort of idea
that they must be collected at once and put back into the jury-box, or
they would die.
'The trial cannot proceed,' said the King in a very grave voice, 'until
all the jurymen are back in their proper places--ALL,' he repeated with
great emphasis, looking hard at Alice as he said do.
Alice looked at the jury-box, and saw that, in her haste, she had put
the Lizard in head downwards, and the poor little thing was waving its
tail about in a melancholy way, being quite unable to move. She soon got
it out again, and put it right; 'not that it signifies much,' she said
to herself; 'I should think it would be QUITE as much use in the trial
one way up as the other.'
As soon as the jury had a little recovered from the shock of being
upset, and their slates and pencils had been found and handed back to
them, they set to work very diligently to write out a history of the
accident, all except the Lizard, who seemed too much overcome to do
anything but sit with its mouth open, gazing up into the roof of the
court.
'What do you know about this business?' the King said to Alice.
'Nothing,' said Alice.
'Nothing WHATEVER?' persisted the King.
'Nothing whatever,' said Alice.
'That's very important,' the King said, turning to the jury. They were
just beginning to write this down on their slates, when the White Rabbit
interrupted: 'UNimportant, your Majesty means, of course,' he said in a
very respectful tone, but frowning and making faces at him as he spoke.
'UNimportant, of course, I meant,' the King hastily said, and went on
to himself in an undertone,
'important--unimportant--unimportant--important--' as if he were trying
which word sounded best.
Some of the jury wrote it down 'important,' and some 'unimportant.'
Alice could see this, as she was near enough to look over their slates;
'but it doesn't matter a bit,' she thought to herself.
At this moment the King, who had been for some time busily writing in
his note-book, cackled out 'Silence!' and read out from his book, 'Rule
Forty-two. ALL PERSONS MORE THAN A MILE HIGH TO LEAVE THE COURT.'
Everybody looked at Alice.
'I'M not a mile high,' said Alice.
'You are,' said the King.
'Nearly two miles high,' added the Queen.
'Well, I shan't go, at any rate,' said Alice: 'besides, that's not a
regular rule: you invented it just now.'
'It's the oldest rule in the book,' said the King.
'Then it ought to be Number One,' said Alice.
The King turned pale, and shut his note-book hastily. 'Consider your
verdict,' he said to the jury, in a low, trembling voice.
'There's more evidence to come yet, please your Majesty,' said the White
Rabbit, jumping up in a great hurry; 'this paper has just been picked
up.'
'What's in it?' said the Queen.
'I haven't opened it yet,' said the White Rabbit, 'but it seems to be a
letter, written by the prisoner to--to somebody.'
'It must have been that,' said the King, 'unless it was written to
nobody, which isn't usual, you know.'
'Who is it directed to?' said one of the jurymen.
'It isn't directed at all,' said the White Rabbit; 'in fact, there's
nothing written on the OUTSIDE.' He unfolded the paper as he spoke, and
added 'It isn't a letter, after all: it's a set of verses.'
'Are they in the prisoner's handwriting?' asked another of the jurymen.
'No, they're not,' said the White Rabbit, 'and that's the queerest thing
about it.' (The jury all looked puzzled.)
'He must have imitated somebody else's hand,' said the King. (The jury
all brightened up again.)
'Please your Majesty,' said the Knave, 'I didn't write it, and they
can't prove I did: there's no name signed at the end.'
'If you didn't sign it,' said the King, 'that only makes the matter
worse. You MUST have meant some mischief, or else you'd have signed your
name like an honest man.'
There was a general clapping of hands at this: it was the first really
clever thing the King had said that day.
'That PROVES his guilt,' said the Queen.
'It proves nothing of the sort!' said Alice. 'Why, you don't even know
what they're about!'
'Read them,' said the King.
The White Rabbit put on his spectacles. 'Where shall I begin, please
your Majesty?' he asked.
'Begin at the beginning,' the King said gravely, 'and go on till you
come to the end: then stop.'
These were the verses the White Rabbit read:--
'They told me you had been to her,
And mentioned me to him:
She gave me a good character,
But said I could not swim.
He sent them word I had not gone
(We know it to be true):
If she should push the matter on,
What would become of you?
I gave her one, they gave him two,
You gave us three or more;
They all returned from him to you,
Though they were mine before.
If I or she should chance to be
Involved in this affair,
He trusts to you to set them free,
Exactly as we were.
My notion was that you had been
(Before she had this fit)
An obstacle that came between
Him, and ourselves, and it.
Don't let him know she liked them best,
For this must ever be
A secret, kept from all the rest,
Between yourself and me.'
'That's the most important piece of evidence we've heard yet,' said the
King, rubbing his hands; 'so now let the jury--'
'If any one of them can explain it,' said Alice, (she had grown so large
in the last few minutes that she wasn't a bit afraid of interrupting
him,) 'I'll give him sixpence. _I_ don't believe there's an atom of
meaning in it.'
The jury all wrote down on their slates, 'SHE doesn't believe there's an
atom of meaning in it,' but none of them attempted to explain the paper.
'If there's no meaning in it,' said the King, 'that saves a world of
trouble, you know, as we needn't try to find any. And yet I don't know,'
he went on, spreading out the verses on his knee, and looking at them
with one eye; 'I seem to see some meaning in them, after all. "--SAID
I COULD NOT SWIM--" you can't swim, can you?' he added, turning to the
Knave.
The Knave shook his head sadly. 'Do I look like it?' he said. (Which he
certainly did NOT, being made entirely of cardboard.)
'All right, so far,' said the King, and he went on muttering over
the verses to himself: '"WE KNOW IT TO BE TRUE--" that's the jury, of
course--"I GAVE HER ONE, THEY GAVE HIM TWO--" why, that must be what he
did with the tarts, you know--'
'But, it goes on "THEY ALL RETURNED FROM HIM TO YOU,"' said Alice.
'Why, there they are!' said the King triumphantly, pointing to the tarts
on the table. 'Nothing can be clearer than THAT. Then again--"BEFORE SHE
HAD THIS FIT--" you never had fits, my dear, I think?' he said to the
Queen.
'Never!' said the Queen furiously, throwing an inkstand at the Lizard
as she spoke. (The unfortunate little Bill had left off writing on his
slate with one finger, as he found it made no mark; but he now hastily
began again, using the ink, that was trickling down his face, as long as
it lasted.)
'Then the words don't FIT you,' said the King, looking round the court
with a smile. There was a dead silence.
'It's a pun!' the King added in an offended tone, and everybody laughed,
'Let the jury consider their verdict,' the King said, for about the
twentieth time that day.
'No, no!' said the Queen. 'Sentence first--verdict afterwards.'
'Stuff and nonsense!' said Alice loudly. 'The idea of having the
sentence first!'
'Hold your tongue!' said the Queen, turning purple.
'I won't!' said Alice.
'Off with her head!' the Queen shouted at the top of her voice. Nobody
moved.
'Who cares for you?' said Alice, (she had grown to her full size by this
time.) 'You're nothing but a pack of cards!'
At this the whole pack rose up into the air, and came flying down upon
her: she gave a little scream, half of fright and half of anger, and
tried to beat them off, and found herself lying on the bank, with her
head in the lap of her sister, who was gently brushing away some dead
leaves that had fluttered down from the trees upon her face.
'Wake up, Alice dear!' said her sister; 'Why, what a long sleep you've
had!'
'Oh, I've had such a curious dream!' said Alice, and she told her
sister, as well as she could remember them, all these strange Adventures
of hers that you have just been reading about; and when she had
finished, her sister kissed her, and said, 'It WAS a curious dream,
dear, certainly: but now run in to your tea; it's getting late.' So
Alice got up and ran off, thinking while she ran, as well she might,
what a wonderful dream it had been.
But her sister sat still just as she left her, leaning her head on her
hand, watching the setting sun, and thinking of little Alice and all her
wonderful Adventures, till she too began dreaming after a fashion, and
this was her dream:--
First, she dreamed of little Alice herself, and once again the tiny
hands were clasped upon her knee, and the bright eager eyes were looking
up into hers--she could hear the very tones of her voice, and see that
queer little toss of her head to keep back the wandering hair that
WOULD always get into her eyes--and still as she listened, or seemed to
listen, the whole place around her became alive with the strange creatures
of her little sister's dream.
The long grass rustled at her feet as the White Rabbit hurried by--the
frightened Mouse splashed his way through the neighbouring pool--she
could hear the rattle of the teacups as the March Hare and his friends
shared their never-ending meal, and the shrill voice of the Queen
ordering off her unfortunate guests to execution--once more the pig-baby
was sneezing on the Duchess's knee, while plates and dishes crashed
around it--once more the shriek of the Gryphon, the squeaking of the
Lizard's slate-pencil, and the choking of the suppressed guinea-pigs,
filled the air, mixed up with the distant sobs of the miserable Mock
Turtle.
So she sat on, with closed eyes, and half believed herself in
Wonderland, though she knew she had but to open them again, and all
would change to dull reality--the grass would be only rustling in the
wind, and the pool rippling to the waving of the reeds--the rattling
teacups would change to tinkling sheep-bells, and the Queen's shrill
cries to the voice of the shepherd boy--and the sneeze of the baby, the
shriek of the Gryphon, and all the other queer noises, would change (she
knew) to the confused clamour of the busy farm-yard--while the lowing
of the cattle in the distance would take the place of the Mock Turtle's
heavy sobs.
Lastly, she pictured to herself how this same little sister of hers
would, in the after-time, be herself a grown woman; and how she would
keep, through all her riper years, the simple and loving heart of her
childhood: and how she would gather about her other little children, and
make THEIR eyes bright and eager with many a strange tale, perhaps even
with the dream of Wonderland of long ago: and how she would feel with
all their simple sorrows, and find a pleasure in all their simple joys,
remembering her own child-life, and the happy summer days.
THE END
================================================
FILE: packages/interface-ipfs-core/test/fixtures/test-folder/files/hello.txt
================================================
Hello
================================================
FILE: packages/interface-ipfs-core/test/fixtures/test-folder/files/ipfs.txt
================================================
IPFS
================================================
FILE: packages/interface-ipfs-core/test/fixtures/test-folder/holmes.txt
================================================
Project Gutenberg's The Adventures of Sherlock Holmes, by Arthur Conan Doyle
This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever. You may copy it, give it away or
re-use it under the terms of the Project Gutenberg License included
with this eBook or online at www.gutenberg.net
Title: The Adventures of Sherlock Holmes
Author: Arthur Conan Doyle
Posting Date: April 18, 2011 [EBook #1661]
First Posted: November 29, 2002
Language: English
*** START OF THIS PROJECT GUTENBERG EBOOK THE ADVENTURES OF SHERLOCK HOLMES ***
Produced by an anonymous Project Gutenberg volunteer and Jose Menendez
THE ADVENTURES OF SHERLOCK HOLMES
by
SIR ARTHUR CONAN DOYLE
I. A Scandal in Bohemia
II. The Red-headed League
III. A Case of Identity
IV. The Boscombe Valley Mystery
V. The Five Orange Pips
VI. The Man with the Twisted Lip
VII. The Adventure of the Blue Carbuncle
VIII. The Adventure of the Speckled Band
IX. The Adventure of the Engineer's Thumb
X. The Adventure of the Noble Bachelor
XI. The Adventure of the Beryl Coronet
XII. The Adventure of the Copper Beeches
ADVENTURE I. A SCANDAL IN BOHEMIA
I.
To Sherlock Holmes she is always THE woman. I have seldom heard
him mention her under any other name. In his eyes she eclipses
and predominates the whole of her sex. It was not that he felt
any emotion akin to love for Irene Adler. All emotions, and that
one particularly, were abhorrent to his cold, precise but
admirably balanced mind. He was, I take it, the most perfect
reasoning and observing machine that the world has seen, but as a
lover he would have placed himself in a false position. He never
spoke of the softer passions, save with a gibe and a sneer. They
were admirable things for the observer--excellent for drawing the
veil from men's motives and actions. But for the trained reasoner
to admit such intrusions into his own delicate and finely
adjusted temperament was to introduce a distracting factor which
might throw a doubt upon all his mental results. Grit in a
sensitive instrument, or a crack in one of his own high-power
lenses, would not be more disturbing than a strong emotion in a
nature such as his. And yet there was but one woman to him, and
that woman was the late Irene Adler, of dubious and questionable
memory.
I had seen little of Holmes lately. My marriage had drifted us
away from each other. My own complete happiness, and the
home-centred interests which rise up around the man who first
finds himself master of his own establishment, were sufficient to
absorb all my attention, while Holmes, who loathed every form of
society with his whole Bohemian soul, remained in our lodgings in
Baker Street, buried among his old books, and alternating from
week to week between cocaine and ambition, the drowsiness of the
drug, and the fierce energy of his own keen nature. He was still,
as ever, deeply attracted by the study of crime, and occupied his
immense faculties and extraordinary powers of observation in
following out those clues, and clearing up those mysteries which
had been abandoned as hopeless by the official police. From time
to time I heard some vague account of his doings: of his summons
to Odessa in the case of the Trepoff murder, of his clearing up
of the singular tragedy of the Atkinson brothers at Trincomalee,
and finally of the mission which he had accomplished so
delicately and successfully for the reigning family of Holland.
Beyond these signs of his activity, however, which I merely
shared with all the readers of the daily press, I knew little of
my former friend and companion.
One night--it was on the twentieth of March, 1888--I was
returning from a journey to a patient (for I had now returned to
civil practice), when my way led me through Baker Street. As I
passed the well-remembered door, which must always be associated
in my mind with my wooing, and with the dark incidents of the
Study in Scarlet, I was seized with a keen desire to see Holmes
again, and to know how he was employing his extraordinary powers.
His rooms were brilliantly lit, and, even as I looked up, I saw
his tall, spare figure pass twice in a dark silhouette against
the blind. He was pacing the room swiftly, eagerly, with his head
sunk upon his chest and his hands clasped behind him. To me, who
knew his every mood and habit, his attitude and manner told their
own story. He was at work again. He had risen out of his
drug-created dreams and was hot upon the scent of some new
problem. I rang the bell and was shown up to the chamber which
had formerly been in part my own.
His manner was not effusive. It seldom was; but he was glad, I
think, to see me. With hardly a word spoken, but with a kindly
eye, he waved me to an armchair, threw across his case of cigars,
and indicated a spirit case and a gasogene in the corner. Then he
stood before the fire and looked me over in his singular
introspective fashion.
"Wedlock suits you," he remarked. "I think, Watson, that you have
put on seven and a half pounds since I saw you."
"Seven!" I answered.
"Indeed, I should have thought a little more. Just a trifle more,
I fancy, Watson. And in practice again, I observe. You did not
tell me that you intended to go into harness."
"Then, how do you know?"
"I see it, I deduce it. How do I know that you have been getting
yourself very wet lately, and that you have a most clumsy and
careless servant girl?"
"My dear Holmes," said I, "this is too much. You would certainly
have been burned, had you lived a few centuries ago. It is true
that I had a country walk on Thursday and came home in a dreadful
mess, but as I have changed my clothes I can't imagine how you
deduce it. As to Mary Jane, she is incorrigible, and my wife has
given her notice, but there, again, I fail to see how you work it
out."
He chuckled to himself and rubbed his long, nervous hands
together.
"It is simplicity itself," said he; "my eyes tell me that on the
inside of your left shoe, just where the firelight strikes it,
the leather is scored by six almost parallel cuts. Obviously they
have been caused by someone who has very carelessly scraped round
the edges of the sole in order to remove crusted mud from it.
Hence, you see, my double deduction that you had been out in vile
weather, and that you had a particularly malignant boot-slitting
specimen of the London slavey. As to your practice, if a
gentleman walks into my rooms smelling of iodoform, with a black
mark of nitrate of silver upon his right forefinger, and a bulge
on the right side of his top-hat to show where he has secreted
his stethoscope, I must be dull, indeed, if I do not pronounce
him to be an active member of the medical profession."
I could not help laughing at the ease with which he explained his
process of deduction. "When I hear you give your reasons," I
remarked, "the thing always appears to me to be so ridiculously
simple that I could easily do it myself, though at each
successive instance of your reasoning I am baffled until you
explain your process. And yet I believe that my eyes are as good
as yours."
"Quite so," he answered, lighting a cigarette, and throwing
himself down into an armchair. "You see, but you do not observe.
The distinction is clear. For example, you have frequently seen
the steps which lead up from the hall to this room."
"Frequently."
"How often?"
"Well, some hundreds of times."
"Then how many are there?"
"How many? I don't know."
"Quite so! You have not observed. And yet you have seen. That is
just my point. Now, I know that there are seventeen steps,
because I have both seen and observed. By-the-way, since you are
interested in these little problems, and since you are good
enough to chronicle one or two of my trifling experiences, you
may be interested in this." He threw over a sheet of thick,
pink-tinted note-paper which had been lying open upon the table.
"It came by the last post," said he. "Read it aloud."
The note was undated, and without either signature or address.
"There will call upon you to-night, at a quarter to eight
o'clock," it said, "a gentleman who desires to consult you upon a
matter of the very deepest moment. Your recent services to one of
the royal houses of Europe have shown that you are one who may
safely be trusted with matters which are of an importance which
can hardly be exaggerated. This account of you we have from all
quarters received. Be in your chamber then at that hour, and do
not take it amiss if your visitor wear a mask."
"This is indeed a mystery," I remarked. "What do you imagine that
it means?"
"I have no data yet. It is a capital mistake to theorize before
one has data. Insensibly one begins to twist facts to suit
theories, instead of theories to suit facts. But the note itself.
What do you deduce from it?"
I carefully examined the writing, and the paper upon which it was
written.
"The man who wrote it was presumably well to do," I remarked,
endeavouring to imitate my companion's processes. "Such paper
could not be bought under half a crown a packet. It is peculiarly
strong and stiff."
"Peculiar--that is the very word," said Holmes. "It is not an
English paper at all. Hold it up to the light."
I did so, and saw a large "E" with a small "g," a "P," and a
large "G" with a small "t" woven into the texture of the paper.
"What do you make of that?" asked Holmes.
"The name of the maker, no doubt; or his monogram, rather."
"Not at all. The 'G' with the small 't' stands for
'Gesellschaft,' which is the German for 'Company.' It is a
customary contraction like our 'Co.' 'P,' of course, stands for
'Papier.' Now for the 'Eg.' Let us glance at our Continental
Gazetteer." He took down a heavy brown volume from his shelves.
"Eglow, Eglonitz--here we are, Egria. It is in a German-speaking
country--in Bohemia, not far from Carlsbad. 'Remarkable as being
the scene of the death of Wallenstein, and for its numerous
glass-factories and paper-mills.' Ha, ha, my boy, what do you
make of that?" His eyes sparkled, and he sent up a great blue
triumphant cloud from his cigarette.
"The paper was made in Bohemia," I said.
"Precisely. And the man who wrote the note is a German. Do you
note the peculiar construction of the sentence--'This account of
you we have from all quarters received.' A Frenchman or Russian
could not have written that. It is the German who is so
uncourteous to his verbs. It only remains, therefore, to discover
what is wanted by this German who writes upon Bohemian paper and
prefers wearing a mask to showing his face. And here he comes, if
I am not mistaken, to resolve all our doubts."
As he spoke there was the sharp sound of horses' hoofs and
grating wheels against the curb, followed by a sharp pull at the
bell. Holmes whistled.
"A pair, by the sound," said he. "Yes," he continued, glancing
out of the window. "A nice little brougham and a pair of
beauties. A hundred and fifty guineas apiece. There's money in
this case, Watson, if there is nothing else."
"I think that I had better go, Holmes."
"Not a bit, Doctor. Stay where you are. I am lost without my
Boswell. And this promises to be interesting. It would be a pity
to miss it."
"But your client--"
"Never mind him. I may want your help, and so may he. Here he
comes. Sit down in that armchair, Doctor, and give us your best
attention."
A slow and heavy step, which had been heard upon the stairs and
in the passage, paused immediately outside the door. Then there
was a loud and authoritative tap.
"Come in!" said Holmes.
A man entered who could hardly have been less than six feet six
inches in height, with the chest and limbs of a Hercules. His
dress was rich with a richness which would, in England, be looked
upon as akin to bad taste. Heavy bands of astrakhan were slashed
across the sleeves and fronts of his double-breasted coat, while
the deep blue cloak which was thrown over his shoulders was lined
with flame-coloured silk and secured at the neck with a brooch
which consisted of a single flaming beryl. Boots which extended
halfway up his calves, and which were trimmed at the tops with
rich brown fur, completed the impression of barbaric opulence
which was suggested by his whole appearance. He carried a
broad-brimmed hat in his hand, while he wore across the upper
part of his face, extending down past the cheekbones, a black
vizard mask, which he had apparently adjusted that very moment,
for his hand was still raised to it as he entered. From the lower
part of the face he appeared to be a man of strong character,
with a thick, hanging lip, and a long, straight chin suggestive
of resolution pushed to the length of obstinacy.
"You had my note?" he asked with a deep harsh voice and a
strongly marked German accent. "I told you that I would call." He
looked from one to the other of us, as if uncertain which to
address.
"Pray take a seat," said Holmes. "This is my friend and
colleague, Dr. Watson, who is occasionally good enough to help me
in my cases. Whom have I the honour to address?"
"You may address me as the Count Von Kramm, a Bohemian nobleman.
I understand that this gentleman, your friend, is a man of honour
and discretion, whom I may trust with a matter of the most
extreme importance. If not, I should much prefer to communicate
with you alone."
I rose to go, but Holmes caught me by the wrist and pushed me
back into my chair. "It is both, or none," said he. "You may say
before this gentleman anything which you may say to me."
The Count shrugged his broad shoulders. "Then I must begin," said
he, "by binding you both to absolute secrecy for two years; at
the end of that time the matter will be of no importance. At
present it is not too much to say that it is of such weight it
may have an influence upon European history."
"I promise," said Holmes.
"And I."
"You will excuse this mask," continued our strange visitor. "The
august person who employs me wishes his agent to be unknown to
you, and I may confess at once that the title by which I have
just called myself is not exactly my own."
"I was aware of it," said Holmes dryly.
"The circumstances are of great delicacy, and every precaution
has to be taken to quench what might grow to be an immense
scandal and seriously compromise one of the reigning families of
Europe. To speak plainly, the matter implicates the great House
of Ormstein, hereditary kings of Bohemia."
"I was also aware of that," murmured Holmes, settling himself
down in his armchair and closing his eyes.
Our visitor glanced with some apparent surprise at the languid,
lounging figure of the man who had been no doubt depicted to him
as the most incisive reasoner and most energetic agent in Europe.
Holmes slowly reopened his eyes and looked impatiently at his
gigantic client.
"If your Majesty would condescend to state your case," he
remarked, "I should be better able to advise you."
The man sprang from his chair and paced up and down the room in
uncontrollable agitation. Then, with a gesture of desperation, he
tore the mask from his face and hurled it upon the ground. "You
are right," he cried; "I am the King. Why should I attempt to
conceal it?"
"Why, indeed?" murmured Holmes. "Your Majesty had not spoken
before I was aware that I was addressing Wilhelm Gottsreich
Sigismond von Ormstein, Grand Duke of Cassel-Felstein, and
hereditary King of Bohemia."
"But you can understand," said our strange visitor, sitting down
once more and passing his hand over his high white forehead, "you
can understand that I am not accustomed to doing such business in
my own person. Yet the matter was so delicate that I could not
confide it to an agent without putting myself in his power. I
have come incognito from Prague for the purpose of consulting
you."
"Then, pray consult," said Holmes, shutting his eyes once more.
"The facts are briefly these: Some five years ago, during a
lengthy visit to Warsaw, I made the acquaintance of the well-known
adventuress, Irene Adler. The name is no doubt familiar to you."
"Kindly look her up in my index, Doctor," murmured Holmes without
opening his eyes. For many years he had adopted a system of
docketing all paragraphs concerning men and things, so that it
was difficult to name a subject or a person on which he could not
at once furnish information. In this case I found her biography
sandwiched in between that of a Hebrew rabbi and that of a
staff-commander who had written a monograph upon the deep-sea
fishes.
"Let me see!" said Holmes. "Hum! Born in New Jersey in the year
1858. Contralto--hum! La Scala, hum! Prima donna Imperial Opera
of Warsaw--yes! Retired from operatic stage--ha! Living in
London--quite so! Your Majesty, as I understand, became entangled
with this young person, wrote her some compromising letters, and
is now desirous of getting those letters back."
"Precisely so. But how--"
"Was there a secret marriage?"
"None."
"No legal papers or certificates?"
"None."
"Then I fail to follow your Majesty. If this young person should
produce her letters for blackmailing or other purposes, how is
she to prove their authenticity?"
"There is the writing."
"Pooh, pooh! Forgery."
"My private note-paper."
"Stolen."
"My own seal."
"Imitated."
"My photograph."
"Bought."
"We were both in the photograph."
"Oh, dear! That is very bad! Your Majesty has indeed committed an
indiscretion."
"I was mad--insane."
"You have compromised yourself seriously."
"I was only Crown Prince then. I was young. I am but thirty now."
"It must be recovered."
"We have tried and failed."
"Your Majesty must pay. It must be bought."
"She will not sell."
"Stolen, then."
"Five attempts have been made. Twice burglars in my pay ransacked
her house. Once we diverted her luggage when she travelled. Twice
she has been waylaid. There has been no result."
"No sign of it?"
"Absolutely none."
Holmes laughed. "It is quite a pretty little problem," said he.
"But a very serious one to me," returned the King reproachfully.
"Very, indeed. And what does she propose to do with the
photograph?"
"To ruin me."
"But how?"
"I am about to be married."
"So I have heard."
"To Clotilde Lothman von Saxe-Meningen, second daughter of the
King of Scandinavia. You may know the strict principles of her
family. She is herself the very soul of delicacy. A shadow of a
doubt as to my conduct would bring the matter to an end."
"And Irene Adler?"
"Threatens to send them the photograph. And she will do it. I
know that she will do it. You do not know her, but she has a soul
of steel. She has the face of the most beautiful of women, and
the mind of the most resolute of men. Rather than I should marry
another woman, there are no lengths to which she would not
go--none."
"You are sure that she has not sent it yet?"
"I am sure."
"And why?"
"Because she has said that she would send it on the day when the
betrothal was publicly proclaimed. That will be next Monday."
"Oh, then we have three days yet," said Holmes with a yawn. "That
is very fortunate, as I have one or two matters of importance to
look into just at present. Your Majesty will, of course, stay in
London for the present?"
"Certainly. You will find me at the Langham under the name of the
Count Von Kramm."
"Then I shall drop you a line to let you know how we progress."
"Pray do so. I shall be all anxiety."
"Then, as to money?"
"You have carte blanche."
"Absolutely?"
"I tell you that I would give one of the provinces of my kingdom
to have that photograph."
"And for present expenses?"
The King took a heavy chamois leather bag from under his cloak
and laid it on the table.
"There are three hundred pounds in gold and seven hundred in
notes," he said.
Holmes scribbled a receipt upon a sheet of his note-book and
handed it to him.
"And Mademoiselle's address?" he asked.
"Is Briony Lodge, Serpentine Avenue, St. John's Wood."
Holmes took a note of it. "One other question," said he. "Was the
photograph a cabinet?"
"It was."
"Then, good-night, your Majesty, and I trust that we shall soon
have some good news for you. And good-night, Watson," he added,
as the wheels of the royal brougham rolled down the street. "If
you will be good enough to call to-morrow afternoon at three
o'clock I should like to chat this little matter over with you."
II.
At three o'clock precisely I was at Baker Street, but Holmes had
not yet returned. The landlady informed me that he had left the
house shortly after eight o'clock in the morning. I sat down
beside the fire, however, with the intention of awaiting him,
however long he might be. I was already deeply interested in his
inquiry, for, though it was surrounded by none of the grim and
strange features which were associated with the two crimes which
I have already recorded, still, the nature of the case and the
exalted station of his client gave it a character of its own.
Indeed, apart from the nature of the investigation which my
friend had on hand, there was something in his masterly grasp of
a situation, and his keen, incisive reasoning, which made it a
pleasure to me to study his system of work, and to follow the
quick, subtle methods by which he disentangled the most
inextricable mysteries. So accustomed was I to his invariable
success that the very possibility of his failing had ceased to
enter into my head.
It was close upon four before the door opened, and a
drunken-looking groom, ill-kempt and side-whiskered, with an
inflamed face and disreputable clothes, walked into the room.
Accustomed as I was to my friend's amazing powers in the use of
disguises, I had to look three times before I was certain that it
was indeed he. With a nod he vanished into the bedroom, whence he
emerged in five minutes tweed-suited and respectable, as of old.
Putting his hands into his pockets, he stretched out his legs in
front of the fire and laughed heartily for some minutes.
"Well, really!" he cried, and then he choked and laughed again
until he was obliged to lie back, limp and helpless, in the
chair.
"What is it?"
"It's quite too funny. I am sure you could never guess how I
employed my morning, or what I ended by doing."
"I can't imagine. I suppose that you have been watching the
habits, and perhaps the house, of Miss Irene Adler."
"Quite so; but the sequel was rather unusual. I will tell you,
however. I left the house a little after eight o'clock this
morning in the character of a groom out of work. There is a
wonderful sympathy and freemasonry among horsey men. Be one of
them, and you will know all that there is to know. I soon found
Briony Lodge. It is a bijou villa, with a garden at the back, but
built out in front right up to the road, two stories. Chubb lock
to the door. Large sitting-room on the right side, well
furnished, with long windows almost to the floor, and those
preposterous English window fasteners which a child could open.
Behind there was nothing remarkable, save that the passage window
could be reached from the top of the coach-house. I walked round
it and examined it closely from every point of view, but without
noting anything else of interest.
"I then lounged down the street and found, as I expected, that
there was a mews in a lane which runs down by one wall of the
garden. I lent the ostlers a hand in rubbing down their horses,
and received in exchange twopence, a glass of half and half, two
fills of shag tobacco, and as much information as I could desire
about Miss Adler, to say nothing of half a dozen other people in
the neighbourhood in whom I was not in the least interested, but
whose biographies I was compelled to listen to."
"And what of Irene Adler?" I asked.
"Oh, she has turned all the men's heads down in that part. She is
the daintiest thing under a bonnet on this planet. So say the
Serpentine-mews, to a man. She lives quietly, sings at concerts,
drives out at five every day, and returns at seven sharp for
dinner. Seldom goes out at other times, except when she sings.
Has only one male visitor, but a good deal of him. He is dark,
handsome, and dashing, never calls less than once a day, and
often twice. He is a Mr. Godfrey Norton, of the Inner Temple. See
the advantages of a cabman as a confidant. They had driven him
home a dozen times from Serpentine-mews, and knew all about him.
When I had listened to all they had to tell, I began to walk up
and down near Briony Lodge once more, and to think over my plan
of campaign.
"This Godfrey Norton was evidently an important factor in the
matter. He was a lawyer. That sounded ominous. What was the
relation between them, and what the object of his repeated
visits? Was she his client, his friend, or his mistress? If the
former, she had probably transferred the photograph to his
keeping. If the latter, it was less likely. On the issue of this
question depended whether I should continue my work at Briony
Lodge, or turn my attention to the gentleman's chambers in the
Temple. It was a delicate point, and it widened the field of my
inquiry. I fear that I bore you with these details, but I have to
let you see my little difficulties, if you are to understand the
situation."
"I am following you closely," I answered.
"I was still balancing the matter in my mind when a hansom cab
drove up to Briony Lodge, and a gentleman sprang out. He was a
remarkably handsome man, dark, aquiline, and moustached--evidently
the man of whom I had heard. He appeared to be in a
great hurry, shouted to the cabman to wait, and brushed past the
maid who opened the door with the air of a man who was thoroughly
at home.
"He was in the house about half an hour, and I could catch
glimpses of him in the windows of the sitting-room, pacing up and
down, talking excitedly, and waving his arms. Of her I could see
nothing. Presently he emerged, looking even more flurried than
before. As he stepped up to the cab, he pulled a gold watch from
his pocket and looked at it earnestly, 'Drive like the devil,' he
shouted, 'first to Gross & Hankey's in Regent Street, and then to
the Church of St. Monica in the Edgeware Road. Half a guinea if
you do it in twenty minutes!'
"Away they went, and I was just wondering whether I should not do
well to follow them when up the lane came a neat little landau,
the coachman with his coat only half-buttoned, and his tie under
his ear, while all the tags of his harness were sticking out of
the buckles. It hadn't pulled up before she shot out of the hall
door and into it. I only caught a glimpse of her at the moment,
but she was a lovely woman, with a face that a man might die for.
"'The Church of St. Monica, John,' she cried, 'and half a
sovereign if you reach it in twenty minutes.'
"This was quite too good to lose, Watson. I was just balancing
whether I should run for it, or whether I should perch behind her
landau when a cab came through the street. The driver looked
twice at such a shabby fare, but I jumped in before he could
object. 'The Church of St. Monica,' said I, 'and half a sovereign
if you reach it in twenty minutes.' It was twenty-five minutes to
twelve, and of course it was clear enough what was in the wind.
"My cabby drove fast. I don't think I ever drove faster, but the
others were there before us. The cab and the landau with their
steaming horses were in front of the door when I arrived. I paid
the man and hurried into the church. There was not a soul there
save the two whom I had followed and a surpliced clergyman, who
seemed to be expostulating with them. They were all three
standing in a knot in front of the altar. I lounged up the side
aisle like any other idler who has dropped into a church.
Suddenly, to my surprise, the three at the altar faced round to
me, and Godfrey Norton came running as hard as he could towards
me.
"'Thank God,' he cried. 'You'll do. Come! Come!'
"'What then?' I asked.
"'Come, man, come, only three minutes, or it won't be legal.'
"I was half-dragged up to the altar, and before I knew where I was
I found myself mumbling responses which were whispered in my ear,
and vouching for things of which I knew nothing, and generally
assisting in the secure tying up of Irene Adler, spinster, to
Godfrey Norton, bachelor. It was all done in an instant, and
there was the gentleman thanking me on the one side and the lady
on the other, while the clergyman beamed on me in front. It was
the most preposterous position in which I ever found myself in my
life, and it was the thought of it that started me laughing just
now. It seems that there had been some informality about their
license, that the clergyman absolutely refused to marry them
without a witness of some sort, and that my lucky appearance
saved the bridegroom from having to sally out into the streets in
search of a best man. The bride gave me a sovereign, and I mean
to wear it on my watch-chain in memory of the occasion."
"This is a very unexpected turn of affairs," said I; "and what
then?"
"Well, I found my plans very seriously menaced. It looked as if
the pair might take an immediate departure, and so necessitate
very prompt and energetic measures on my part. At the church
door, however, they separated, he driving back to the Temple, and
she to her own house. 'I shall drive out in the park at five as
usual,' she said as she left him. I heard no more. They drove
away in different directions, and I went off to make my own
arrangements."
"Which are?"
"Some cold beef and a glass of beer," he answered, ringing the
bell. "I have been too busy to think of food, and I am likely to
be busier still this evening. By the way, Doctor, I shall want
your co-operation."
"I shall be delighted."
"You don't mind breaking the law?"
"Not in the least."
"Nor running a chance of arrest?"
"Not in a good cause."
"Oh, the cause is excellent!"
"Then I am your man."
"I was sure that I might rely on you."
"But what is it you wish?"
"When Mrs. Turner has brought in the tray I will make it clear to
you. Now," he said as he turned hungrily on the simple fare that
our landlady had provided, "I must discuss it while I eat, for I
have not much time. It is nearly five now. In two hours we must
be on the scene of action. Miss Irene, or Madame, rather, returns
from her drive at seven. We must be at Briony Lodge to meet her."
"And what then?"
"You must leave that to me. I have already arranged what is to
occur. There is only one point on which I must insist. You must
not interfere, come what may. You understand?"
"I am to be neutral?"
"To do nothing whatever. There will probably be some small
unpleasantness. Do not join in it. It will end in my being
conveyed into the house. Four or five minutes afterwards the
sitting-room window will open. You are to station yourself close
to that open window."
"Yes."
"You are to watch me, for I will be visible to you."
"Yes."
"And when I raise my hand--so--you will throw into the room what
I give you to throw, and will, at the same time, raise the cry of
fire. You quite follow me?"
"Entirely."
"It is nothing very formidable," he said, taking a long cigar-shaped
roll from his pocket. "It is an ordinary plumber's smoke-rocket,
fitted with a cap at either end to make it self-lighting.
Your task is confined to that. When you raise your cry of fire,
it will be taken up by quite a number of people. You may then
walk to the end of the street, and I will rejoin you in ten
minutes. I hope that I have made myself clear?"
"I am to remain neutral, to get near the window, to watch you,
and at the signal to throw in this object, then to raise the cry
of fire, and to wait you at the corner of the street."
"Precisely."
"Then you may entirely rely on me."
"That is excellent. I think, perhaps, it is almost time that I
prepare for the new role I have to play."
He disappeared into his bedroom and returned in a few minutes in
the character of an amiable and simple-minded Nonconformist
clergyman. His broad black hat, his baggy trousers, his white
tie, his sympathetic smile, and general look of peering and
benevolent curiosity were such as Mr. John Hare alone could have
equalled. It was not merely that Holmes changed his costume. His
expression, his manner, his very soul seemed to vary with every
fresh part that he assumed. The stage lost a fine actor, even as
science lost an acute reasoner, when he became a specialist in
crime.
It was a quarter past six when we left Baker Street, and it still
wanted ten minutes to the hour when we found ourselves in
Serpentine Avenue. It was already dusk, and the lamps were just
being lighted as we paced up and down in front of Briony Lodge,
waiting for the coming of its occupant. The house was just such
as I had pictured it from Sherlock Holmes' succinct description,
but the locality appeared to be less private than I expected. On
the contrary, for a small street in a quiet neighbourhood, it was
remarkably animated. There was a group of shabbily dressed men
smoking and laughing in a corner, a scissors-grinder with his
wheel, two guardsmen who were flirting with a nurse-girl, and
several well-dressed young men who were lounging up and down with
cigars in their mouths.
"You see," remarked Holmes, as we paced to and fro in front of
the house, "this marriage rather simplifies matters. The
photograph becomes a double-edged weapon now. The chances are
that she would be as averse to its being seen by Mr. Godfrey
Norton, as our client is to its coming to the eyes of his
princess. Now the question is, Where are we to find the
photograph?"
"Where, indeed?"
"It is most unlikely that she carries it about with her. It is
cabinet size. Too large for easy concealment about a woman's
dress. She knows that the King is capable of having her waylaid
and searched. Two attempts of the sort have already been made. We
may take it, then, that she does not carry it about with her."
"Where, then?"
"Her banker or her lawyer. There is that double possibility. But
I am inclined to think neither. Women are naturally secretive,
and they like to do their own secreting. Why should she hand it
over to anyone else? She could trust her own guardianship, but
she could not tell what indirect or political influence might be
brought to bear upon a business man. Besides, remember that she
had resolved to use it within a few days. It must be where she
can lay her hands upon it. It must be in her own house."
"But it has twice been burgled."
"Pshaw! They did not know how to look."
"But how will you look?"
"I will not look."
"What then?"
"I will get her to show me."
"But she will refuse."
"She will not be able to. But I hear the rumble of wheels. It is
her carriage. Now carry out my orders to the letter."
As he spoke the gleam of the side-lights of a carriage came round
the curve of the avenue. It was a smart little landau which
rattled up to the door of Briony Lodge. As it pulled up, one of
the loafing men at the corner dashed forward to open the door in
the hope of earning a copper, but was elbowed away by another
loafer, who had rushed up with the same intention. A fierce
quarrel broke out, which was increased by the two guardsmen, who
took sides with one of the loungers, and by the scissors-grinder,
who was equally hot upon the other side. A blow was struck, and
in an instant the lady, who had stepped from her carriage, was
the centre of a little knot of flushed and struggling men, who
struck savagely at each other with their fists and sticks. Holmes
dashed into the crowd to protect the lady; but just as he reached
her he gave a cry and dropped to the ground, with the blood
running freely down his face. At his fall the guardsmen took to
their heels in one direction and the loungers in the other, while
a number of better-dressed people, who had watched the scuffle
without taking part in it, crowded in to help the lady and to
attend to the injured man. Irene Adler, as I will still call her,
had hurried up the steps; but she stood at the top with her
superb figure outlined against the lights of the hall, looking
back into the street.
"Is the poor gentleman much hurt?" she asked.
"He is dead," cried several voices.
"No, no, there's life in him!" shouted another. "But he'll be
gone before you can get him to hospital."
"He's a brave fellow," said a woman. "They would have had the
lady's purse and watch if it hadn't been for him. They were a
gang, and a rough one, too. Ah, he's breathing now."
"He can't lie in the street. May we bring him in, marm?"
"Surely. Bring him into the sitting-room. There is a comfortable
sofa. This way, please!"
Slowly and solemnly he was borne into Briony Lodge and laid out
in the principal room, while I still observed the proceedings
from my post by the window. The lamps had been lit, but the
blinds had not been drawn, so that I could see Holmes as he lay
upon the couch. I do not know whether he was seized with
compunction at that moment for the part he was playing, but I
know that I never felt more heartily ashamed of myself in my life
than when I saw the beautiful creature against whom I was
conspiring, or the grace and kindliness with which she waited
upon the injured man. And yet it would be the blackest treachery
to Holmes to draw back now from the part which he had intrusted
to me. I hardened my heart, and took the smoke-rocket from under
my ulster. After all, I thought, we are not injuring her. We are
but preventing her from injuring another.
Holmes had sat up upon the couch, and I saw him motion like a man
who is in need of air. A maid rushed across and threw open the
window. At the same instant I saw him raise his hand and at the
signal I tossed my rocket into the room with a cry of "Fire!" The
word was no sooner out of my mouth than the whole crowd of
spectators, well dressed and ill--gentlemen, ostlers, and
servant-maids--joined in a general shriek of "Fire!" Thick clouds
of smoke curled through the room and out at the open window. I
caught a glimpse of rushing figures, and a moment later the voice
of Holmes from within assuring them that it was a false alarm.
Slipping through the shouting crowd I made my way to the corner
of the street, and in ten minutes was rejoiced to find my
friend's arm in mine, and to get away from the scene of uproar.
He walked swiftly and in silence for some few minutes until we
had turned down one of the quiet streets which lead towards the
Edgeware Road.
"You did it very nicely, Doctor," he remarked. "Nothing could
have been better. It is all right."
"You have the photograph?"
"I know where it is."
"And how did you find out?"
"She showed me, as I told you she would."
"I am still in the dark."
"I do not wish to make a mystery," said he, laughing. "The matter
was perfectly simple. You, of course, saw that everyone in the
street was an accomplice. They were all engaged for the evening."
"I guessed as much."
"Then, when the row broke out, I had a little moist red paint in
the palm of my hand. I rushed forward, fell down, clapped my hand
to my face, and became a piteous spectacle. It is an old trick."
"That also I could fathom."
"Then they carried me in. She was bound to have me in. What else
could she do? And into her sitting-room, which was the very room
which I suspected. It lay between that and her bedroom, and I was
determined to see which. They laid me on a couch, I motioned for
air, they were compelled to open the window, and you had your
chance."
"How did that help you?"
"It was all-important. When a woman thinks that her house is on
fire, her instinct is at once to rush to the thing which she
values most. It is a perfectly overpowering impulse, and I have
more than once taken advantage of it. In the case of the
Darlington substitution scandal it was of use to me, and also in
the Arnsworth Castle business. A married woman grabs at her baby;
an unmarried one reaches for her jewel-box. Now it was clear to
me that our lady of to-day had nothing in the house more precious
to her than what we are in quest of. She would rush to secure it.
The alarm of fire was admirably done. The smoke and shouting were
enough to shake nerves of steel. She responded beautifully. The
photograph is in a recess behind a sliding panel just above the
right bell-pull. She was there in an instant, and I caught a
glimpse of it as she half-drew it out. When I cried out that it
was a false alarm, she replaced it, glanced at the rocket, rushed
from the room, and I have not seen her since. I rose, and, making
my excuses, escaped from the house. I hesitated whether to
attempt to secure the photograph at once; but the coachman had
come in, and as he was watching me narrowly it seemed safer to
wait. A little over-precipitance may ruin all."
"And now?" I asked.
"Our quest is practically finished. I shall call with the King
to-morrow, and with you, if you care to come with us. We will be
shown into the sitting-room to wait for the lady, but it is
probable that when she comes she may find neither us nor the
photograph. It might be a satisfaction to his Majesty to regain
it with his own hands."
"And when will you call?"
"At eight in the morning. She will not be up, so that we shall
have a clear field. Besides, we must be prompt, for this marriage
may mean a complete change in her life and habits. I must wire to
the King without delay."
We had reached Baker Street and had stopped at the door. He was
searching his pockets for the key when someone passing said:
"Good-night, Mister Sherlock Holmes."
There were several people on the pavement at the time, but the
greeting appeared to come from a slim youth in an ulster who had
hurried by.
"I've heard that voice before," said Holmes, staring down the
dimly lit street. "Now, I wonder who the deuce that could have
been."
III.
I slept at Baker Street that night, and we were engaged upon our
toast and coffee in the morning when the King of Bohemia rushed
into the room.
"You have really got it!" he cried, grasping Sherlock Holmes by
either shoulder and looking eagerly into his face.
"Not yet."
"But you have hopes?"
"I have hopes."
"Then, come. I am all impatience to be gone."
"We must have a cab."
"No, my brougham is waiting."
"Then that will simplify matters." We descended and started off
once more for Briony Lodge.
"Irene Adler is married," remarked Holmes.
"Married! When?"
"Yesterday."
"But to whom?"
"To an English lawyer named Norton."
"But she could not love him."
"I am in hopes that she does."
"And why in hopes?"
"Because it would spare your Majesty all fear of future
annoyance. If the lady loves her husband, she does not love your
Majesty. If she does not love your Majesty, there is no reason
why she should interfere with your Majesty's plan."
"It is true. And yet--Well! I wish she had been of my own
station! What a queen she would have made!" He relapsed into a
moody silence, which was not broken until we drew up in
Serpentine Avenue.
The door of Briony Lodge was open, and an elderly woman stood
upon the steps. She watched us with a sardonic eye as we stepped
from the brougham.
"Mr. Sherlock Holmes, I believe?" said she.
"I am Mr. Holmes," answered my companion, looking at her with a
questioning and rather startled gaze.
"Indeed! My mistress told me that you were likely to call. She
left this morning with her husband by the 5:15 train from Charing
Cross for the Continent."
"What!" Sherlock Holmes staggered back, white with chagrin and
surprise. "Do you mean that she has left England?"
"Never to return."
"And the papers?" asked the King hoarsely. "All is lost."
"We shall see." He pushed past the servant and rushed into the
drawing-room, followed by the King and myself. The furniture was
scattered about in every direction, with dismantled shelves and
open drawers, as if the lady had hurriedly ransacked them before
her flight. Holmes rushed at the bell-pull, tore back a small
sliding shutter, and, plunging in his hand, pulled out a
photograph and a letter. The photograph was of Irene Adler
herself in evening dress, the letter was superscribed to
"Sherlock Holmes, Esq. To be left till called for." My friend
tore it open and we all three read it together. It was dated at
midnight of the preceding night and ran in this way:
"MY DEAR MR. SHERLOCK HOLMES,--You really did it very well. You
took me in completely. Until after the alarm of fire, I had not a
suspicion. But then, when I found how I had betrayed myself, I
began to think. I had been warned against you months ago. I had
been told that if the King employed an agent it would certainly
be you. And your address had been given me. Yet, with all this,
you made me reveal what you wanted to know. Even after I became
suspicious, I found it hard to think evil of such a dear, kind
old clergyman. But, you know, I have been trained as an actress
myself. Male costume is nothing new to me. I often take advantage
of the freedom which it gives. I sent John, the coachman, to
watch you, ran up stairs, got into my walking-clothes, as I call
them, and came down just as you departed.
"Well, I followed you to your door, and so made sure that I was
really an object of interest to the celebrated Mr. Sherlock
Holmes. Then I, rather imprudently, wished you good-night, and
started for the Temple to see my husband.
"We both thought the best resource was flight, when pursued by
so formidable an antagonist; so you will find the nest empty when
you call to-morrow. As to the photograph, your client may rest in
peace. I love and am loved by a better man than he. The King may
do what he will without hindrance from one whom he has cruelly
wronged. I keep it only to safeguard myself, and to preserve a
weapon which will always secure me from any steps which he might
take in the future. I leave a photograph which he might care to
possess; and I remain, dear Mr. Sherlock Holmes,
"Very truly yours,
"IRENE NORTON, née ADLER."
"What a woman--oh, what a woman!" cried the King of Bohemia, when
we had all three read this epistle. "Did I not tell you how quick
and resolute she was? Would she not have made an admirable queen?
Is it not a pity that she was not on my level?"
"From what I have seen of the lady she seems indeed to be on a
very different level to your Majesty," said Holmes coldly. "I am
sorry that I have not been able to bring your Majesty's business
to a more successful conclusion."
"On the contrary, my dear sir," cried the King; "nothing could be
more successful. I know that her word is inviolate. The
photograph is now as safe as if it were in the fire."
"I am glad to hear your Majesty say so."
"I am immensely indebted to you. Pray tell me in what way I can
reward you. This ring--" He slipped an emerald snake ring from
his finger and held it out upon the palm of his hand.
"Your Majesty has something which I should value even more
highly," said Holmes.
"You have but to name it."
"This photograph!"
The King stared at him in amazement.
"Irene's photograph!" he cried. "Certainly, if you wish it."
"I thank your Majesty. Then there is no more to be done in the
matter. I have the honour to wish you a very good-morning." He
bowed, and, turning away without observing the hand which the
King had stretched out to him, he set off in my company for his
chambers.
And that was how a great scandal threatened to affect the kingdom
of Bohemia, and how the best plans of Mr. Sherlock Holmes were
beaten by a woman's wit. He used to make merry over the
cleverness of women, but I have not heard him do it of late. And
when he speaks of Irene Adler, or when he refers to her
photograph, it is always under the honourable title of the woman.
ADVENTURE II. THE RED-HEADED LEAGUE
I had called upon my friend, Mr. Sherlock Holmes, one day in the
autumn of last year and found him in deep conversation with a
very stout, florid-faced, elderly gentleman with fiery red hair.
With an apology for my intrusion, I was about to withdraw when
Holmes pulled me abruptly into the room and closed the door
behind me.
"You could not possibly have come at a better time, my dear
Watson," he said cordially.
"I was afraid that you were engaged."
"So I am. Very much so."
"Then I can wait in the next room."
"Not at all. This gentleman, Mr. Wilson, has been my partner and
helper in many of my most successful cases, and I have no
doubt that he will be of the utmost use to me in yours also."
The stout gentleman half rose from his chair and gave a bob of
greeting, with a quick little questioning glance from his small
fat-encircled eyes.
"Try the settee," said Holmes, relapsing into his armchair and
putting his fingertips together, as was his custom when in
judicial moods. "I know, my dear Watson, that you share my love
of all that is bizarre and outside the conventions and humdrum
routine of everyday life. You have shown your relish for it by
the enthusiasm which has prompted you to chronicle, and, if you
will excuse my saying so, somewhat to embellish so many of my own
little adventures."
"Your cases have indeed been of the greatest interest to me," I
observed.
"You will remember that I remarked the other day, just before we
went into the very simple problem presented by Miss Mary
Sutherland, that for strange effects and extraordinary
combinations we must go to life itself, which is always far more
daring than any effort of the imagination."
"A proposition which I took the liberty of doubting."
"You did, Doctor, but none the less you must come round to my
view, for otherwise I shall keep on piling fact upon fact on you
until your reason breaks down under them and acknowledges me to
be right. Now, Mr. Jabez Wilson here has been good enough to call
upon me this morning, and to begin a narrative which promises to
be one of the most singular which I have listened to for some
time. You have heard me remark that the strangest and most unique
things are very often connected not with the larger but with the
smaller crimes, and occasionally, indeed, where there is room for
doubt whether any positive crime has been committed. As far as I
have heard it is impossible for me to say whether the present
case is an instance of crime or not, but the course of events is
certainly among the most singular that I have ever listened to.
Perhaps, Mr. Wilson, you would have the great kindness to
recommence your narrative. I ask you not merely because my friend
Dr. Watson has not heard the opening part but also because the
peculiar nature of the story makes me anxious to have every
possible detail from your lips. As a rule, when I have heard some
slight indication of the course of events, I am able to guide
myself by the thousands of other similar cases which occur to my
memory. In the present instance I am forced to admit that the
facts are, to the best of my belief, unique."
The portly client puffed out his chest with an appearance of some
little pride and pulled a dirty and wrinkled newspaper from the
inside pocket of his greatcoat. As he glanced down the
advertisement column, with his head thrust forward and the paper
flattened out upon his knee, I took a good look at the man and
endeavoured, after the fashion of my companion, to read the
indications which might be presented by his dress or appearance.
I did not gain very much, however, by my inspection. Our visitor
bore every mark of being an average commonplace British
tradesman, obese, pompous, and slow. He wore rather baggy grey
shepherd's check trousers, a not over-clean black frock-coat,
unbuttoned in the front, and a drab waistcoat with a heavy brassy
Albert chain, and a square pierced bit of metal dangling down as
an ornament. A frayed top-hat and a faded brown overcoat with a
wrinkled velvet collar lay upon a chair beside him. Altogether,
look as I would, there was nothing remarkable about the man save
his blazing red head, and the expression of extreme chagrin and
discontent upon his features.
Sherlock Holmes' quick eye took in my occupation, and he shook
his head with a smile as he noticed my questioning glances.
"Beyond the obvious facts that he has at some time done manual
labour, that he takes snuff, that he is a Freemason, that he has
been in China, and that he has done a considerable amount of
writing lately, I can deduce nothing else."
Mr. Jabez Wilson started up in his chair, with his forefinger
upon the paper, but his eyes upon my companion.
"How, in the name of good-fortune, did you know all that, Mr.
Holmes?" he asked. "How did you know, for example, that I did
manual labour. It's as true as gospel, for I began as a ship's
carpenter."
"Your hands, my dear sir. Your right hand is quite a size larger
than your left. You have worked with it, and the muscles are more
developed."
"Well, the snuff, then, and the Freemasonry?"
"I won't insult your intelligence by telling you how I read that,
especially as, rather against the strict rules of your order, you
use an arc-and-compass breastpin."
"Ah, of course, I forgot that. But the writing?"
"What else can be indicated by that right cuff so very shiny for
five inches, and the left one with the smooth patch near the
elbow where you rest it upon the desk?"
"Well, but China?"
"The fish that you have tattooed immediately above your right
wrist could only have been done in China. I have made a small
study of tattoo marks and have even contributed to the literature
of the subject. That trick of staining the fishes' scales of a
delicate pink is quite peculiar to China. When, in addition, I
see a Chinese coin hanging from your watch-chain, the matter
becomes even more simple."
Mr. Jabez Wilson laughed heavily. "Well, I never!" said he. "I
thought at first that you had done something clever, but I see
that there was nothing in it, after all."
"I begin to think, Watson," said Holmes, "that I make a mistake
in explaining. 'Omne ignotum pro magnifico,' you know, and my
poor little reputation, such as it is, will suffer shipwreck if I
am so candid. Can you not find the advertisement, Mr. Wilson?"
"Yes, I have got it now," he answered with his thick red finger
planted halfway down the column. "Here it is. This is what began
it all. You just read it for yourself, sir."
I took the paper from him and read as follows:
"TO THE RED-HEADED LEAGUE: On account of the bequest of the late
Ezekiah Hopkins, of Lebanon, Pennsylvania, U. S. A., there is now
another vacancy open which entitles a member of the League to a
salary of 4 pounds a week for purely nominal services. All
red-headed men who are sound in body and mind and above the age
of twenty-one years, are eligible. Apply in person on Monday, at
eleven o'clock, to Duncan Ross, at the offices of the League, 7
Pope's Court, Fleet Street."
"What on earth does this mean?" I ejaculated after I had twice
read over the extraordinary announcement.
Holmes chuckled and wriggled in his chair, as was his habit when
in high spirits. "It is a little off the beaten track, isn't it?"
said he. "And now, Mr. Wilson, off you go at scratch and tell us
all about yourself, your household, and the effect which this
advertisement had upon your fortunes. You will first make a note,
Doctor, of the paper and the date."
"It is The Morning Chronicle of April 27, 1890. Just two months
ago."
"Very good. Now, Mr. Wilson?"
"Well, it is just as I have been telling you, Mr. Sherlock
Holmes," said Jabez Wilson, mopping his forehead; "I have a small
pawnbroker's business at Coburg Square, near the City. It's not a
very large affair, and of late years it has not done more than
just give me a living. I used to be able to keep two assistants,
but now I only keep one; and I would have a job to pay him but
that he is willing to come for half wages so as to learn the
business."
"What is the name of this obliging youth?" asked Sherlock Holmes.
"His name is Vincent Spaulding, and he's not such a youth,
either. It's hard to say his age. I should not wish a smarter
assistant, Mr. Holmes; and I know very well that he could better
himself and earn twice what I am able to give him. But, after
all, if he is satisfied, why should I put ideas in his head?"
"Why, indeed? You seem most fortunate in having an employé who
comes under the full market price. It is not a common experience
among employers in this age. I don't know that your assistant is
not as remarkable as your advertisement."
"Oh, he has his faults, too," said Mr. Wilson. "Never was such a
fellow for photography. Snapping away with a camera when he ought
to be improving his mind, and then diving down into the cellar
like a rabbit into its hole to develop his pictures. That is his
main fault, but on the whole he's a good worker. There's no vice
in him."
"He is still with you, I presume?"
"Yes, sir. He and a girl of fourteen, who does a bit of simple
cooking and keeps the place clean--that's all I have in the
house, for I am a widower and never had any family. We live very
quietly, sir, the three of us; and we keep a roof over our heads
and pay our debts, if we do nothing more.
"The first thing that put us out was that advertisement.
Spaulding, he came down into the office just this day eight
weeks, with this very paper in his hand, and he says:
"'I wish to the Lord, Mr. Wilson, that I was a red-headed man.'
"'Why that?' I asks.
"'Why,' says he, 'here's another vacancy on the League of the
Red-headed Men. It's worth quite a little fortune to any man who
gets it, and I understand that there are more vacancies than
there are men, so that the trustees are at their wits' end what
to do with the money. If my hair would only change colour, here's
a nice little crib all ready for me to step into.'
"'Why, what is it, then?' I asked. You see, Mr. Holmes, I am a
very stay-at-home man, and as my business came to me instead of
my having to go to it, I was often weeks on end without putting
my foot over the door-mat. In that way I didn't know much of what
was going on outside, and I was always glad of a bit of news.
"'Have you never heard of the League of the Red-headed Men?' he
asked with his eyes open.
"'Never.'
"'Why, I wonder at that, for you are eligible yourself for one
of the vacancies.'
"'And what are they worth?' I asked.
"'Oh, merely a couple of hundred a year, but the work is slight,
and it need not interfere very much with one's other
occupations.'
"Well, you can easily think that that made me prick up my ears,
for the business has not been over-good for some years, and an
extra couple of hundred would have been very handy.
"'Tell me all about it,' said I.
"'Well,' said he, showing me the advertisement, 'you can see for
yourself that the League has a vacancy, and there is the address
where you should apply for particulars. As far as I can make out,
the League was founded by an American millionaire, Ezekiah
Hopkins, who was very peculiar in his ways. He was himself
red-headed, and he had a great sympathy for all red-headed men;
so when he died it was found that he had left his enormous
fortune in the hands of trustees, with instructions to apply the
interest to the providing of easy berths to men whose hair is of
that colour. From all I hear it is splendid pay and very little to
do.'
"'But,' said I, 'there would be millions of red-headed men who
would apply.'
"'Not so many as you might think,' he answered. 'You see it is
really confined to Londoners, and to grown men. This American had
started from London when he was young, and he wanted to do the
old town a good turn. Then, again, I have heard it is no use your
applying if your hair is light red, or dark red, or anything but
real bright, blazing, fiery red. Now, if you cared to apply, Mr.
Wilson, you would just walk in; but perhaps it would hardly be
worth your while to put yourself out of the way for the sake of a
few hundred pounds.'
"Now, it is a fact, gentlemen, as you may see for yourselves,
that my hair is of a very full and rich tint, so that it seemed
to me that if there was to be any competition in the matter I
stood as good a chance as any man that I had ever met. Vincent
Spaulding seemed to know so much about it that I thought he might
prove useful, so I just ordered him to put up the shutters for
the day and to come right away with me. He was very willing to
have a holiday, so we shut the business up and started off for
the address that was given us in the advertisement.
"I never hope to see such a sight as that again, Mr. Holmes. From
north, south, east, and west every man who had a shade of red in
his hair had tramped into the city to answer the advertisement.
Fleet Street was choked with red-headed folk, and Pope's Court
looked like a coster's orange barrow. I should not have thought
there were so many in the whole country as were brought together
by that single advertisement. Every shade of colour they
were--straw, lemon, orange, brick, Irish-setter, liver, clay;
but, as Spaulding said, there were not many who had the real
vivid flame-coloured tint. When I saw how many were waiting, I
would have given it up in despair; but Spaulding would not hear
of it. How he did it I could not imagine, but he pushed and
pulled and butted until he got me through the crowd, and right up
to the steps which led to the office. There was a double stream
upon the stair, some going up in hope, and some coming back
dejected; but we wedged in as well as we could and soon found
ourselves in the office."
"Your experience has been a most entertaining one," remarked
Holmes as his client paused and refreshed his memory with a huge
pinch of snuff. "Pray continue your very interesting statement."
"There was nothing in the office but a couple of wooden chairs
and a deal table, behind which sat a small man with a head that
was even redder than mine. He said a few words to each candidate
as he came up, and then he always managed to find some fault in
them which would disqualify them. Getting a vacancy did not seem
to be such a very easy matter, after all. However, when our turn
came the little man was much more favourable to me than to any of
the others, and he closed the door as we entered, so that he
might have a private word with us.
"'This is Mr. Jabez Wilson,' said my assistant, 'and he is
willing to fill a vacancy in the League.'
"'And he is admirably suited for it,' the other answered. 'He has
every requirement. I cannot recall when I have seen anything so
fine.' He took a step backward, cocked his head on one side, and
gazed at my hair until I felt quite bashful. Then suddenly he
plunged forward, wrung my hand, and congratulated me warmly on my
success.
"'It would be injustice to hesitate,' said he. 'You will,
however, I am sure, excuse me for taking an obvious precaution.'
With that he seized my hair in both his hands, and tugged until I
yelled with the pain. 'There is water in your eyes,' said he as
he released me. 'I perceive that all is as it should be. But we
have to be careful, for we have twice been deceived by wigs and
once by paint. I could tell you tales of cobbler's wax which
would disgust you with human nature.' He stepped over to the
window and shouted through it at the top of his voice that the
vacancy was filled. A groan of disappointment came up from below,
and the folk all trooped away in different directions until there
was not a red-head to be seen except my own and that of the
manager.
"'My name,' said he, 'is Mr. Duncan Ross, and I am myself one of
the pensioners upon the fund left by our noble benefactor. Are
you a married man, Mr. Wilson? Have you a family?'
"I answered that I had not.
"His face fell immediately.
"'Dear me!' he said gravely, 'that is very serious indeed! I am
sorry to hear you say that. The fund was, of course, for the
propagation and spread of the red-heads as well as for their
maintenance. It is exceedingly unfortunate that you should be a
bachelor.'
"My face lengthened at this, Mr. Holmes, for I thought that I was
not to have the vacancy after all; but after thinking it over for
a few minutes he said that it would be all right.
"'In the case of another,' said he, 'the objection might be
fatal, but we must stretch a point in favour of a man with such a
head of hair as yours. When shall you be able to enter upon your
new duties?'
"'Well, it is a little awkward, for I have a business already,'
said I.
"'Oh, never mind about that, Mr. Wilson!' said Vincent Spaulding.
'I should be able to look after that for you.'
"'What would be the hours?' I asked.
"'Ten to two.'
"Now a pawnbroker's business is mostly done of an evening, Mr.
Holmes, especially Thursday and Friday evening, which is just
before pay-day; so it would suit me very well to earn a little in
the mornings. Besides, I knew that my assistant was a good man,
and that he would see to anything that turned up.
"'That would suit me very well,' said I. 'And the pay?'
"'Is 4 pounds a week.'
"'And the work?'
"'Is purely nominal.'
"'What do you call purely nominal?'
"'Well, you have to be in the office, or at least in the
building, the whole time. If you leave, you forfeit your whole
position forever. The will is very clear upon that point. You
don't comply with the conditions if you budge from the office
during that time.'
"'It's only four hours a day, and I should not think of leaving,'
said I.
"'No excuse will avail,' said Mr. Duncan Ross; 'neither sickness
nor business nor anything else. There you must stay, or you lose
your billet.'
"'And the work?'
"'Is to copy out the "Encyclopaedia Britannica." There is the first
volume of it in that press. You must find your own ink, pens, and
blotting-paper, but we provide this table and chair. Will you be
ready to-morrow?'
"'Certainly,' I answered.
"'Then, good-bye, Mr. Jabez Wilson, and let me congratulate you
once more on the important position which you have been fortunate
enough to gain.' He bowed me out of the room and I went home with
my assistant, hardly knowing what to say or do, I was so pleased
at my own good fortune.
"Well, I thought over the matter all day, and by evening I was in
low spirits again; for I had quite persuaded myself that the
whole affair must be some great hoax or fraud, though what its
object might be I could not imagine. It seemed altogether past
belief that anyone could make such a will, or that they would pay
such a sum for doing anything so simple as copying out the
'Encyclopaedia Britannica.' Vincent Spaulding did what he could to
cheer me up, but by bedtime I had reasoned myself out of the
whole thing. However, in the morning I determined to have a look
at it anyhow, so I bought a penny bottle of ink, and with a
quill-pen, and seven sheets of foolscap paper, I started off for
Pope's Court.
"Well, to my surprise and delight, everything was as right as
possible. The table was set out ready for me, and Mr. Duncan Ross
was there to see that I got fairly to work. He started me off
upon the letter A, and then he left me; but he would drop in from
time to time to see that all was right with me. At two o'clock he
bade me good-day, complimented me upon the amount that I had
written, and locked the door of the office after me.
"This went on day after day, Mr. Holmes, and on Saturday the
manager came in and planked down four golden sovereigns for my
week's work. It was the same next week, and the same the week
after. Every morning I was there at ten, and every afternoon I
left at two. By degrees Mr. Duncan Ross took to coming in only
once of a morning, and then, after a time, he did not come in at
all. Still, of course, I never dared to leave the room for an
instant, for I was not sure when he might come, and the billet
was such a good one, and suited me so well, that I would not risk
the loss of it.
"Eight weeks passed away like this, and I had written about
Abbots and Archery and Armour and Architecture and Attica, and
hoped with diligence that I might get on to the B's before very
long. It cost me something in foolscap, and I had pretty nearly
filled a shelf with my writings. And then suddenly the whole
business came to an end."
"To an end?"
"Yes, sir. And no later than this morning. I went to my work as
usual at ten o'clock, but the door was shut and locked, with a
little square of cardboard hammered on to the middle of the
panel with a tack. Here it is, and you can read for yourself."
He held up a piece of white cardboard about the size of a sheet
of note-paper. It read in this fashion:
THE RED-HEADED LEAGUE
IS
DISSOLVED.
October 9, 1890.
Sherlock Holmes and I surveyed this curt announcement and the
rueful face behind it, until the comical side of the affair so
completely overtopped every other consideration that we both
burst out into a roar of laughter.
"I cannot see that there is anything very funny," cried our
client, flushing up to the roots of his flaming head. "If you can
do nothing better than laugh at me, I can go elsewhere."
"No, no," cried Holmes, shoving him back into the chair from
which he had half risen. "I really wouldn't miss your case for
the world. It is most refreshingly unusual. But there is, if you
will excuse my saying so, something just a little funny about it.
Pray what steps did you take when you found the card upon the
door?"
"I was staggered, sir. I did not know what to do. Then I called
at the offices round, but none of them seemed to know anything
about it. Finally, I went to the landlord, who is an accountant
living on the ground-floor, and I asked him if he could tell me
what had become of the Red-headed League. He said that he had
never heard of any such body. Then I asked him who Mr. Duncan
Ross was. He answered that the name was new to him.
"'Well,' said I, 'the gentleman at No. 4.'
"'What, the red-headed man?'
"'Yes.'
"'Oh,' said he, 'his name was William Morris. He was a solicitor
and was using my room as a temporary convenience until his new
premises were ready. He moved out yesterday.'
"'Where could I find him?'
"'Oh, at his new offices. He did tell me the address. Yes, 17
King Edward Street, near St. Paul's.'
"I started off, Mr. Holmes, but when I got to that address it was
a manufactory of artificial knee-caps, and no one in it had ever
heard of either Mr. William Morris or Mr. Duncan Ross."
"And what did you do then?" asked Holmes.
"I went home to Saxe-Coburg Square, and I took the advice of my
assistant. But he could not help me in any way. He could only say
that if I waited I should hear by post. But that was not quite
good enough, Mr. Holmes. I did not wish to lose such a place
without a struggle, so, as I had heard that you were good enough
to give advice to poor folk who were in need of it, I came right
away to you."
"And you did very wisely," said Holmes. "Your case is an
exceedingly remarkable one, and I shall be happy to look into it.
From what you have told me I think that it is possible that
graver issues hang from it than might at first sight appear."
"Grave enough!" said Mr. Jabez Wilson. "Why, I have lost four
pound a week."
"As far as you are personally concerned," remarked Holmes, "I do
not see that you have any grievance against this extraordinary
league. On the contrary, you are, as I understand, richer by some
30 pounds, to say nothing of the minute knowledge which you have
gained on every subject which comes under the letter A. You have
lost nothing by them."
"No, sir. But I want to find out about them, and who they are,
and what their object was in playing this prank--if it was a
prank--upon me. It was a pretty expensive joke for them, for it
cost them two and thirty pounds."
"We shall endeavour to clear up these points for you. And, first,
one or two questions, Mr. Wilson. This assistant of yours who
first called your attention to the advertisement--how long had he
been with you?"
"About a month then."
"How did he come?"
"In answer to an advertisement."
"Was he the only applicant?"
"No, I had a dozen."
"Why did you pick him?"
"Because he was handy and would come cheap."
"At half-wages, in fact."
"Yes."
"What is he like, this Vincent Spaulding?"
"Small, stout-built, very quick in his ways, no hair on his face,
though he's not short of thirty. Has a white splash of acid upon
his forehead."
Holmes sat up in his chair in considerable excitement. "I thought
as much," said he. "Have you ever observed that his ears are
pierced for earrings?"
"Yes, sir. He told me that a gipsy had done it for him when he
was a lad."
"Hum!" said Holmes, sinking back in deep thought. "He is still
with you?"
"Oh, yes, sir; I have only just left him."
"And has your business been attended to in your absence?"
"Nothing to complain of, sir. There's never very much to do of a
morning."
"That will do, Mr. Wilson. I shall be happy to give you an
opinion upon the subject in the course of a day or two. To-day is
Saturday, and I hope that by Monday we may come to a conclusion."
"Well, Watson," said Holmes when our visitor had left us, "what
do you make of it all?"
"I make nothing of it," I answered frankly. "It is a most
mysterious business."
"As a rule," said Holmes, "the more bizarre a thing is the less
mysterious it proves to be. It is your commonplace, featureless
crimes which are really puzzling, just as a commonplace face is
the most difficult to identify. But I must be prompt over this
matter."
"What are you going to do, then?" I asked.
"To smoke," he answered. "It is quite a three pipe problem, and I
beg that you won't speak to me for fifty minutes." He curled
himself up in his chair, with his thin knees drawn up to his
hawk-like nose, and there he sat with his eyes closed and his
black clay pipe thrusting out like the bill of some strange bird.
I had come to the conclusion that he had dropped asleep, and
indeed was nodding myself, when he suddenly sprang out of his
chair with the gesture of a man who has made up his mind and put
his pipe down upon the mantelpiece.
"Sarasate plays at the St. James's Hall this afternoon," he
remarked. "What do you think, Watson? Could your patients spare
you for a few hours?"
"I have nothing to do to-day. My practice is never very
absorbing."
"Then put on your hat and come. I am going through the City
first, and we can have some lunch on the way. I observe that
there is a good deal of German music on the programme, which is
rather more to my taste than Italian or French. It is
introspective, and I want to introspect. Come along!"
We travelled by the Underground as far as Aldersgate; and a short
walk took us to Saxe-Coburg Square, the scene of the singular
story which we had listened to in the morning. It was a poky,
little, shabby-genteel place, where four lines of dingy
two-storied brick houses looked out into a small railed-in
enclosure, where a lawn of weedy grass and a few clumps of faded
laurel-bushes made a hard fight against a smoke-laden and
uncongenial atmosphere. Three gilt balls and a brown board with
"JABEZ WILSON" in white letters, upon a corner house, announced
the place where our red-headed client carried on his business.
Sherlock Holmes stopped in front of it with his head on one side
and looked it all over, with his eyes shining brightly between
puckered lids. Then he walked slowly up the street, and then down
again to the corner, still looking keenly at the houses. Finally
he returned to the pawnbroker's, and, having thumped vigorously
upon the pavement with his stick two or three times, he went up
to the door and knocked. It was instantly opened by a
bright-looking, clean-shaven young fellow, who asked him to step
in.
"Thank you," said Holmes, "I only wished to ask you how you would
go from here to the Strand."
"Third right, fourth left," answered the assistant promptly,
closing the door.
"Smart fellow, that," observed Holmes as we walked away. "He is,
in my judgment, the fourth smartest man in London, and for daring
I am not sure that he has not a claim to be third. I have known
something of him before."
"Evidently," said I, "Mr. Wilson's assistant counts for a good
deal in this mystery of the Red-headed League. I am sure that you
inquired your way merely in order that you might see him."
"Not him."
"What then?"
"The knees of his trousers."
"And what did you see?"
"What I expected to see."
"Why did you beat the pavement?"
"My dear doctor, this is a time for observation, not for talk. We
are spies in an enemy's country. We know something of Saxe-Coburg
Square. Let us now explore the parts which lie behind it."
The road in which we found ourselves as we turned round the
corner from the retired Saxe-Coburg Square presented as great a
contrast to it as the front of a picture does to the back. It was
one of the main arteries which conveyed the traffic of the City
to the north and west. The roadway was blocked with the immense
stream of commerce flowing in a double tide inward and outward,
while the footpaths were black with the hurrying swarm of
pedestrians. It was difficult to realise as we looked at the line
of fine shops and stately business premises that they really
abutted on the other side upon the faded and stagnant square
which we had just quitted.
"Let me see," said Holmes, standing at the corner and glancing
along the line, "I should like just to remember the order of the
houses here. It is a hobby of mine to have an exact knowledge of
London. There is Mortimer's, the tobacconist, the little
newspaper shop, the Coburg branch of the City and Suburban Bank,
the Vegetarian Restaurant, and McFarlane's carriage-building
depot. That carries us right on to the other block. And now,
Doctor, we've done our work, so it's time we had some play. A
sandwich and a cup of coffee, and then off to violin-land, where
all is sweetness and delicacy and harmony, and there are no
red-headed clients to vex us with their conundrums."
My friend was an enthusiastic musician, being himself not only a
very capable performer but a composer of no ordinary merit. All
the afternoon he sat in the stalls wrapped in the most perfect
happiness, gently waving his long, thin fingers in time to the
music, while his gently smiling face and his languid, dreamy eyes
were as unlike those of Holmes the sleuth-hound, Holmes the
relentless, keen-witted, ready-handed criminal agent, as it was
possible to conceive. In his singular character the dual nature
alternately asserted itself, and his extreme exactness and
astuteness represented, as I have often thought, the reaction
against the poetic and contemplative mood which occasionally
predominated in him. The swing of his nature took him from
extreme languor to devouring energy; and, as I knew well, he was
never so truly formidable as when, for days on end, he had been
lounging in his armchair amid his improvisations and his
black-letter editions. Then it was that the lust of the chase
would suddenly come upon him, and that his brilliant reasoning
power would rise to the level of intuition, until those who were
unacquainted with his methods would look askance at him as on a
man whose knowledge was not that of other mortals. When I saw him
that afternoon so enwrapped in the music at St. James's Hall I
felt that an evil time might be coming upon those whom he had set
himself to hunt down.
"You want to go home, no doubt, Doctor," he remarked as we
emerged.
"Yes, it would be as well."
"And I have some business to do which will take some hours. This
business at Coburg Square is serious."
"Why serious?"
"A considerable crime is in contemplation. I have every reason to
believe that we shall be in time to stop it. But to-day being
Saturday rather complicates matters. I shall want your help
to-night."
"At what time?"
"Ten will be early enough."
"I shall be at Baker Street at ten."
"Very well. And, I say, Doctor, there may be some little danger,
so kindly put your army revolver in your pocket." He waved his
hand, turned on his heel, and disappeared in an instant among the
crowd.
I trust that I am not more dense than my neighbours, but I was
always oppressed with a sense of my own stupidity in my dealings
with Sherlock Holmes. Here I had heard what he had heard, I had
seen what he had seen, and yet from his words it was evident that
he saw clearly not only what had happened but what was about to
happen, while to me the whole business was still confused and
grotesque. As I drove home to my house in Kensington I thought
over it all, from the extraordinary story of the red-headed
copier of the "Encyclopaedia" down to the visit to Saxe-Coburg
Square, and the ominous words with which he had parted from me.
What was this nocturnal expedition, and why should I go armed?
Where were we going, and what were we to do? I had the hint from
Holmes that this smooth-faced pawnbroker's assistant was a
formidable man--a man who might play a deep game. I tried to
puzzle it out, but gave it up in despair and set the matter aside
until night should bring an explanation.
It was a quarter-past nine when I started from home and made my
way across the Park, and so through Oxford Street to Baker
Street. Two hansoms were standing at the door, and as I entered
the passage I heard the sound of voices from above. On entering
his room I found Holmes in animated conversation with two men,
one of whom I recognised as Peter Jones, the official police
agent, while the other was a long, thin, sad-faced man, with a
very shiny hat and oppressively respectable frock-coat.
"Ha! Our party is complete," said Holmes, buttoning up his
pea-jacket and taking his heavy hunting crop from the rack.
"Watson, I think you know Mr. Jones, of Scotland Yard? Let me
introduce you to Mr. Merryweather, who is to be our companion in
to-night's adventure."
"We're hunting in couples again, Doctor, you see," said Jones in
his consequential way. "Our friend here is a wonderful man for
starting a chase. All he wants is an old dog to help him to do
the running down."
"I hope a wild goose may not prove to be the end of our chase,"
observed Mr. Merryweather gloomily.
"You may place considerable confidence in Mr. Holmes, sir," said
the police agent loftily. "He has his own little methods, which
are, if he won't mind my saying so, just a little too theoretical
and fantastic, but he has the makings of a detective in him. It
is not too much to say that once or twice, as in that business of
the Sholto murder and the Agra treasure, he has been more nearly
correct than the official force."
"Oh, if you say so, Mr. Jones, it is all right," said the
stranger with deference. "Still, I confess that I miss my rubber.
It is the first Saturday night for seven-and-twenty years that I
have not had my rubber."
"I think you will find," said Sherlock Holmes, "that you will
play for a higher stake to-night than you have ever done yet, and
that the play will be more exciting. For you, Mr. Merryweather,
the stake will be some 30,000 pounds; and for you, Jones, it will
be the man upon whom you wish to lay your hands."
"John Clay, the murderer, thief, smasher, and forger. He's a
young man, Mr. Merryweather, but he is at the head of his
profession, and I would rather have my bracelets on him than on
any criminal in London. He's a remarkable man, is young John
Clay. His grandfather was a royal duke, and he himself has been
to Eton and Oxford. His brain is as cunning as his fingers, and
though we meet signs of him at every turn, we never know where to
find the man himself. He'll crack a crib in Scotland one week,
and be raising money to build an orphanage in Cornwall the next.
I've been on his track for years and have never set eyes on him
yet."
"I hope that I may have the pleasure of introducing you to-night.
I've had one or two little turns also with Mr. John Clay, and I
agree with you that he is at the head of his profession. It is
past ten, however, and quite time that we started. If you two
will take the first hansom, Watson and I will follow in the
second."
Sherlock Holmes was not very communicative during the long drive
and lay back in the cab humming the tunes which he had heard in
the afternoon. We rattled through an endless labyrinth of gas-lit
streets until we emerged into Farrington Street.
"We are close there now," my friend remarked. "This fellow
Merryweather is a bank director, and personally interested in the
matter. I thought it as well to have Jones with us also. He is
not a bad fellow, though an absolute imbecile in his profession.
He has one positive virtue. He is as brave as a bulldog and as
tenacious as a lobster if he gets his claws upon anyone. Here we
are, and they are waiting for us."
We had reached the same crowded thoroughfare in which we had
found ourselves in the morning. Our cabs were dismissed, and,
following the guidance of Mr. Merryweather, we passed down a
narrow passage and through a side door, which he opened for us.
Within there was a small corridor, which ended in a very massive
iron gate. This also was opened, and led down a flight of winding
stone steps, which terminated at another formidable gate. Mr.
Merryweather stopped to light a lantern, and then conducted us
down a dark, earth-smelling passage, and so, after opening a
third door, into a huge vault or cellar, which was piled all
round with crates and massive boxes.
"You are not very vulnerable from above," Holmes remarked as he
held up the lantern and gazed about him.
"Nor from below," said Mr. Merryweather, striking his stick upon
the flags which lined the floor. "Why, dear me, it sounds quite
hollow!" he remarked, looking up in surprise.
"I must really ask you to be a little more quiet!" said Holmes
severely. "You have already imperilled the whole success of our
expedition. Might I beg that you would have the goodness to sit
down upon one of those boxes, and not to interfere?"
The solemn Mr. Merryweather perched himself upon a crate, with a
very injured expression upon his face, while Holmes fell upon his
knees upon the floor and, with the lantern and a magnifying lens,
began to examine minutely the cracks between the stones. A few
seconds sufficed to satisfy him, for he sprang to his feet again
and put his glass in his pocket.
"We have at least an hour before us," he remarked, "for they can
hardly take any steps until the good pawnbroker is safely in bed.
Then they will not lose a minute, for the sooner they do their
work the longer time they will have for their escape. We are at
present, Doctor--as no doubt you have divined--in the cellar of
the City branch of one of the principal London banks. Mr.
Merryweather is the chairman of directors, and he will explain to
you that there are reasons why the more daring criminals of
London should take a considerable interest in this cellar at
present."
"It is our French gold," whispered the director. "We have had
several warnings that an attempt might be made upon it."
"Your French gold?"
"Yes. We had occasion some months ago to strengthen our resources
and borrowed for that purpose 30,000 napoleons from the Bank of
France. It has become known that we have never had occasion to
unpack the money, and that it is still lying in our cellar. The
crate upon which I sit contains 2,000 napoleons packed between
layers of lead foil. Our reserve of bullion is much larger at
present than is usually kept in a single branch office, and the
directors have had misgivings upon the subject."
"Which were very well justified," observed Holmes. "And now it is
time that we arranged our little plans. I expect that within an
hour matters will come to a head. In the meantime Mr.
Merryweather, we must put the screen over that dark lantern."
"And sit in the dark?"
"I am afraid so. I had brought a pack of cards in my pocket, and
I thought that, as we were a partie carrée, you might have your
rubber after all. But I see that the enemy's preparations have
gone so far that we cannot risk the presence of a light. And,
first of all, we must choose our positions. These are daring men,
and though we shall take them at a disadvantage, they may do us
some harm unless we are careful. I shall stand behind this crate,
and do you conceal yourselves behind those. Then, when I flash a
light upon them, close in swiftly. If they fire, Watson, have no
compunction about shooting them down."
I placed my revolver, cocked, upon the top of the wooden case
behind which I crouched. Holmes shot the slide across the front
of his lantern and left us in pitch darkness--such an absolute
darkness as I have never before experienced. The smell of hot
metal remained to assure us that the light was still there, ready
to flash out at a moment's notice. To me, with my nerves worked
up to a pitch of expectancy, there was something depressing and
subduing in the sudden gloom, and in the cold dank air of the
vault.
"They have but one retreat," whispered Holmes. "That is back
through the house into Saxe-Coburg Square. I hope that you have
done what I asked you, Jones?"
"I have an inspector and two officers waiting at the front door."
"Then we have stopped all the holes. And now we must be silent
and wait."
What a time it seemed! From comparing notes afterwards it was but
an hour and a quarter, yet it appeared to me that the night must
have almost gone and the dawn be breaking above us. My limbs
were weary and stiff, for I feared to change my position; yet my
nerves were worked up to the highest pitch of tension, and my
hearing was so acute that I could not only hear the gentle
breathing of my companions, but I could distinguish the deeper,
heavier in-breath of the bulky Jones from the thin, sighing note
of the bank director. From my position I could look over the case
in the direction of the floor. Suddenly my eyes caught the glint
of a light.
At first it was but a lurid spark upon the stone pavement. Then
it lengthened out until it became a yellow line, and then,
without any warning or sound, a gash seemed to open and a hand
appeared, a white, almost womanly hand, which felt about in the
centre of the little area of light. For a minute or more the
hand, with its writhing fingers, protruded out of the floor. Then
it was withdrawn as suddenly as it appeared, and all was dark
again save the single lurid spark which marked a chink between
the stones.
Its disappearance, however, was but momentary. With a rending,
tearing sound, one of the broad, white stones turned over upon
its side and left a square, gaping hole, through which streamed
the light of a lantern. Over the edge there peeped a clean-cut,
boyish face, which looked keenly about it, and then, with a hand
on either side of the aperture, drew itself shoulder-high and
waist-high, until one knee rested upon the edge. In another
instant he stood at the side of the hole and was hauling after
him a companion, lithe and small like himself, with a pale face
and a shock of very red hair.
"It's all clear," he whispered. "Have you the chisel and the
bags? Great Scott! Jump, Archie, jump, and I'll swing for it!"
Sherlock Holmes had sprung out and seized the intruder by the
collar. The other dived down the hole, and I heard the sound of
rending cloth as Jones clutched at his skirts. The light flashed
upon the barrel of a revolver, but Holmes' hunting crop came
down on the man's wrist, and the pistol clinked upon the stone
floor.
"It's no use, John Clay," said Holmes blandly. "You have no
chance at all."
"So I see," the other answered with the utmost coolness. "I fancy
that my pal is all right, though I see you have got his
coat-tails."
"There are three men waiting for him at the door," said Holmes.
"Oh, indeed! You seem to have done the thing very completely. I
must compliment you."
"And I you," Holmes answered. "Your red-headed idea was very new
and effective."
"You'll see your pal again presently," said Jones. "He's quicker
at climbing down holes than I am. Just hold out while I fix the
derbies."
"I beg that you will not touch me with your filthy hands,"
remarked our prisoner as the handcuffs clattered upon his wrists.
"You may not be aware that I have royal blood in my veins. Have
the goodness, also, when you address me always to say 'sir' and
'please.'"
"All right," said Jones with a stare and a snigger. "Well, would
you please, sir, march upstairs, where we can get a cab to carry
your Highness to the police-station?"
"That is better," said John Clay serenely. He made a sweeping bow
to the three of us and walked quietly off in the custody of the
detective.
"Really, Mr. Holmes," said Mr. Merryweather as we followed them
from the cellar, "I do not know how the bank can thank you or
repay you. There is no doubt that you have detected and defeated
in the most complete manner one of the most determined attempts
at bank robbery that have ever come within my experience."
"I have had one or two little scores of my own to settle with Mr.
John Clay," said Holmes. "I have been at some small expense over
this matter, which I shall expect the bank to refund, but beyond
that I am amply repaid by having had an experience which is in
many ways unique, and by hearing the very remarkable narrative of
the Red-headed League."
"You see, Watson," he explained in the early hours of the morning
as we sat over a glass of whisky and soda in Baker Street, "it
was perfectly obvious from the first that the only possible
object of this rather fantastic business of the advertisement of
the League, and the copying of the 'Encyclopaedia,' must be to get
this not over-bright pawnbroker out of the way for a number of
hours every day. It was a curious way of managing it, but,
really, it would be difficult to suggest a better. The method was
no doubt suggested to Clay's ingenious mind by the colour of his
accomplice's hair. The 4 pounds a week was a lure which must draw
him, and what was it to them, who were playing for thousands?
They put in the advertisement, one rogue has the temporary
office, the other rogue incites the man to apply for it, and
together they manage to secure his absence every morning in the
week. From the time that I heard of the assistant having come for
half wages, it was obvious to me that he had some strong motive
for securing the situation."
"But how could you guess what the motive was?"
"Had there been women in the house, I should have suspected a
mere vulgar intrigue. That, however, was out of the question. The
man's business was a small one, and there was nothing in his
house which could account for such elaborate preparations, and
such an expenditure as they were at. It must, then, be something
out of the house. What could it be? I thought of the assistant's
fondness for photography, and his trick of vanishing into the
cellar. The cellar! There was the end of this tangled clue. Then
I made inquiries as to this mysterious assistant and found that I
had to deal with one of the coolest and most daring criminals in
London. He was doing something in the cellar--something which
took many hours a day for months on end. What could it be, once
more? I could think of nothing save that he was running a tunnel
to some other building.
"So far I had got when we went to visit the scene of action. I
surprised you by beating upon the pavement with my stick. I was
ascertaining whether the cellar stretched out in front or behind.
It was not in front. Then I rang the bell, and, as I hoped, the
assistant answered it. We have had some skirmishes, but we had
never set eyes upon each other before. I hardly looked at his
face. His knees were what I wished to see. You must yourself have
remarked how worn, wrinkled, and stained they were. They spoke of
those hours of burrowing. The only remaining point was what they
were burrowing for. I walked round the corner, saw the City and
Suburban Bank abutted on our friend's premises, and felt that I
had solved my problem. When you drove home after the concert I
called upon Scotland Yard and upon the chairman of the bank
directors, with the result that you have seen."
"And how could you tell that they would make their attempt
to-night?" I asked.
"Well, when they closed their League offices that was a sign that
they cared no longer about Mr. Jabez Wilson's presence--in other
words, that they had completed their tunnel. But it was essential
that they should use it soon, as it might be discovered, or the
bullion might be removed. Saturday would suit them better than
any other day, as it would give them two days for their escape.
For all these reasons I expected them to come to-night."
"You reasoned it out beautifully," I exclaimed in unfeigned
admiration. "It is so long a chain, and yet every link rings
true."
"It saved me from ennui," he answered, yawning. "Alas! I already
feel it closing in upon me. My life is spent in one long effort
to escape from the commonplaces of existence. These little
problems help me to do so."
"And you are a benefactor of the race," said I.
He shrugged his shoulders. "Well, perhaps, after all, it is of
some little use," he remarked. "'L'homme c'est rien--l'oeuvre
c'est tout,' as Gustave Flaubert wrote to George Sand."
ADVENTURE III. A CASE OF IDENTITY
"My dear fellow," said Sherlock Holmes as we sat on either side
of the fire in his lodgings at Baker Street, "life is infinitely
stranger than anything which the mind of man could invent. We
would not dare to conceive the things which are really mere
commonplaces of existence. If we could fly out of that window
hand in hand, hover over this great city, gently remove the
roofs, and peep in at the queer things which are going on, the
strange coincidences, the plannings, the cross-purposes, the
wonderful chains of events, working through generations, and
leading to the most outré results, it would make all fiction with
its conventionalities and foreseen conclusions most stale and
unprofitable."
"And yet I am not convinced of it," I answered. "The cases which
come to light in the papers are, as a rule, bald enough, and
vulgar enough. We have in our police reports realism pushed to
its extreme limits, and yet the result is, it must be confessed,
neither fascinating nor artistic."
"A certain selection and discretion must be used in producing a
realistic effect," remarked Holmes. "This is wanting in the
police report, where more stress is laid, perhaps, upon the
platitudes of the magistrate than upon the details, which to an
observer contain the vital essence of the whole matter. Depend
upon it, there is nothing so unnatural as the commonplace."
I smiled and shook my head. "I can quite understand your thinking
so," I said. "Of course, in your position of unofficial adviser
and helper to everybody who is absolutely puzzled, throughout
three continents, you are brought in contact with all that is
strange and bizarre. But here"--I picked up the morning paper
from the ground--"let us put it to a practical test. Here is the
first heading upon which I come. 'A husband's cruelty to his
wife.' There is half a column of print, but I know without
reading it that it is all perfectly familiar to me. There is, of
course, the other woman, the drink, the push, the blow, the
bruise, the sympathetic sister or landlady. The crudest of
writers could invent nothing more crude."
"Indeed, your example is an unfortunate one for your argument,"
said Holmes, taking the paper and glancing his eye down it. "This
is the Dundas separation case, and, as it happens, I was engaged
in clearing up some small points in connection with it. The
husband was a teetotaler, there was no other woman, and the
conduct complained of was that he had drifted into the habit of
winding up every meal by taking out his false teeth and hurling
them at his wife, which, you will allow, is not an action likely
to occur to the imagination of the average story-teller. Take a
pinch of snuff, Doctor, and acknowledge that I have scored over
you in your example."
He held out his snuffbox of old gold, with a great amethyst in
the centre of the lid. Its splendour was in such contrast to his
homely ways and simple life that I could not help commenting upon
it.
"Ah," said he, "I forgot that I had not seen you for some weeks.
It is a little souvenir from the King of Bohemia in return for my
assistance in the case of the Irene Adler papers."
"And the ring?" I asked, glancing at a remarkable brilliant which
sparkled upon his finger.
"It was from the reigning family of Holland, though the matter in
which I served them was of such delicacy that I cannot confide it
even to you, who have been good enough to chronicle one or two of
my little problems."
"And have you any on hand just now?" I asked with interest.
"Some ten or twelve, but none which present any feature of
interest. They are important, you understand, without being
interesting. Indeed, I have found that it is usually in
unimportant matters that there is a field for the observation,
and for the quick analysis of cause and effect which gives the
charm to an investigation. The larger crimes are apt to be the
simpler, for the bigger the crime the more obvious, as a rule, is
the motive. In these cases, save for one rather intricate matter
which has been referred to me from Marseilles, there is nothing
which presents any features of interest. It is possible, however,
that I may have something better before very many minutes are
over, for this is one of my clients, or I am much mistaken."
He had risen from his chair and was standing between the parted
blinds gazing down into the dull neutral-tinted London street.
Looking over his shoulder, I saw that on the pavement opposite
there stood a large woman with a heavy fur boa round her neck,
and a large curling red feather in a broad-brimmed hat which was
tilted in a coquettish Duchess of Devonshire fashion over her
ear. From under this great panoply she peeped up in a nervous,
hesitating fashion at our windows, while her body oscillated
backward and forward, and her fingers fidgeted with her glove
buttons. Suddenly, with a plunge, as of the swimmer who leaves
the bank, she hurried across the road, and we heard the sharp
clang of the bell.
"I have seen those symptoms before," said Holmes, throwing his
cigarette into the fire. "Oscillation upon the pavement always
means an affaire de coeur. She would like advice, but is not sure
that the matter is not too delicate for communication. And yet
even here we may discriminate. When a woman has been seriously
wronged by a man she no longer oscillates, and the usual symptom
is a broken bell wire. Here we may take it that there is a love
matter, but that the maiden is not so much angry as perplexed, or
grieved. But here she comes in person to resolve our doubts."
As he spoke there was a tap at the door, and the boy in buttons
entered to announce Miss Mary Sutherland, while the lady herself
loomed behind his small black figure like a full-sailed
merchant-man behind a tiny pilot boat. Sherlock Holmes welcomed
her with the easy courtesy for which he was remarkable, and,
having closed the door and bowed her into an armchair, he looked
her over in the minute and yet abstracted fashion which was
peculiar to him.
"Do you not find," he said, "that with your short sight it is a
little trying to do so much typewriting?"
"I did at first," she answered, "but now I know where the letters
are without looking." Then, suddenly realising the full purport
of his words, she gave a violent start and looked up, with fear
and astonishment upon her broad, good-humoured face. "You've
heard about me, Mr. Holmes," she cried, "else how could you know
all that?"
"Never mind," said Holmes, laughing; "it is my business to know
things. Perhaps I have trained myself to see what others
overlook. If not, why should you come to consult me?"
"I came to you, sir, because I heard of you from Mrs. Etherege,
whose husband you found so easy when the police and everyone had
given him up for dead. Oh, Mr. Holmes, I wish you would do as
much for me. I'm not rich, but still I have a hundred a year in
my own right, besides the little that I make by the machine, and
I would give it all to know what has become of Mr. Hosmer Angel."
"Why did you come away to consult me in such a hurry?" asked
Sherlock Holmes, with his finger-tips together and his eyes to
the ceiling.
Again a startled look came over the somewhat vacuous face of Miss
Mary Sutherland. "Yes, I did bang out of the house," she said,
"for it made me angry to see the easy way in which Mr.
Windibank--that is, my father--took it all. He would not go to
the police, and he would not go to you, and so at last, as he
would do nothing and kept on saying that there was no harm done,
it made me mad, and I just on with my things and came right away
to you."
"Your father," said Holmes, "your stepfather, surely, since the
name is different."
"Yes, my stepfather. I call him father, though it sounds funny,
too, for he is only five years and two months older than myself."
"And your mother is alive?"
"Oh, yes, mother is alive and well. I wasn't best pleased, Mr.
Holmes, when she married again so soon after father's death, and
a man who was nearly fifteen years younger than herself. Father
was a plumber in the Tottenham Court Road, and he left a tidy
business behind him, which mother carried on with Mr. Hardy, the
foreman; but when Mr. Windibank came he made her sell the
business, for he was very superior, being a traveller in wines.
They got 4700 pounds for the goodwill and interest, which wasn't
near as much as father could have got if he had been alive."
I had expected to see Sherlock Holmes impatient under this
rambling and inconsequential narrative, but, on the contrary, he
had listened with the greatest concentration of attention.
"Your own little income," he asked, "does it come out of the
business?"
"Oh, no, sir. It is quite separate and was left me by my uncle
Ned in Auckland. It is in New Zealand stock, paying 4 1/2 per
cent. Two thousand five hundred pounds was the amount, but I can
only touch the interest."
"You interest me extremely," said Holmes. "And since you draw so
large a sum as a hundred a year, with what you earn into the
bargain, you no doubt travel a little and indulge yourself in
every way. I believe that a single lady can get on very nicely
upon an income of about 60 pounds."
"I could do with much less than that, Mr. Holmes, but you
understand that as long as I live at home I don't wish to be a
burden to them, and so they have the use of the money just while
I am staying with them. Of course, that is only just for the
time. Mr. Windibank draws my interest every quarter and pays it
over to mother, and I find that I can do pretty well with what I
earn at typewriting. It brings me twopence a sheet, and I can
often do from fifteen to twenty sheets in a day."
"You have made your position very clear to me," said Holmes.
"This is my friend, Dr. Watson, before whom you can speak as
freely as before myself. Kindly tell us now all about your
connection with Mr. Hosmer Angel."
A flush stole over Miss Sutherland's face, and she picked
nervously at the fringe of her jacket. "I met him first at the
gasfitters' ball," she said. "They used to send father tickets
when he was alive, and then afterwards they remembered us, and
sent them to mother. Mr. Windibank did not wish us to go. He
never did wish us to go anywhere. He would get quite mad if I
wanted so much as to join a Sunday-school treat. But this time I
was set on going, and I would go; for what right had he to
prevent? He said the folk were not fit for us to know, when all
father's friends were to be there. And he said that I had nothing
fit to wear, when I had my purple plush that I had never so much
as taken out of the drawer. At last, when nothing else would do,
he went off to France upon the business of the firm, but we went,
mother and I, with Mr. Hardy, who used to be our foreman, and it
was there I met Mr. Hosmer Angel."
"I suppose," said Holmes, "that when Mr. Windibank came back from
France he was very annoyed at your having gone to the ball."
"Oh, well, he was very good about it. He laughed, I remember, and
shrugged his shoulders, and said there was no use denying
anything to a woman, for she would have her way."
"I see. Then at the gasfitters' ball you met, as I understand, a
gentleman called Mr. Hosmer Angel."
"Yes, sir. I met him that night, and he called next day to ask if
we had got home all safe, and after that we met him--that is to
say, Mr. Holmes, I met him twice for walks, but after that father
came back again, and Mr. Hosmer Angel could not come to the house
any more."
"No?"
"Well, you know father didn't like anything of the sort. He
wouldn't have any visitors if he could help it, and he used to
say that a woman should be happy in her own family circle. But
then, as I used to say to mother, a woman wants her own circle to
begin with, and I had not got mine yet."
"But how about Mr. Hosmer Angel? Did he make no attempt to see
you?"
"Well, father was going off to France again in a week, and Hosmer
wrote and said that it would be safer and better not to see each
other until he had gone. We could write in the meantime, and he
used to write every day. I took the letters in in the morning, so
there was no need for father to know."
"Were you engaged to the gentleman at this time?"
"Oh, yes, Mr. Holmes. We were engaged after the first walk that
we took. Hosmer--Mr. Angel--was a cashier in an office in
Leadenhall Street--and--"
"What office?"
"That's the worst of it, Mr. Holmes, I don't know."
"Where did he live, then?"
"He slept on the premises."
"And you don't know his address?"
"No--except that it was Leadenhall Street."
"Where did you address your letters, then?"
"To the Leadenhall Street Post Office, to be left till called
for. He said that if they were sent to the office he would be
chaffed by all the other clerks about having letters from a lady,
so I offered to typewrite them, like he did his, but he wouldn't
have that, for he said that when I wrote them they seemed to come
from me, but when they were typewritten he always felt that the
machine had come between us. That will just show you how fond he
was of me, Mr. Holmes, and the little things that he would think
of."
"It was most suggestive," said Holmes. "It has long been an axiom
of mine that the little things are infinitely the most important.
Can you remember any other little things about Mr. Hosmer Angel?"
"He was a very shy man, Mr. Holmes. He would rather walk with me
in the evening than in the daylight, for he said that he hated to
be conspicuous. Very retiring and gentlemanly he was. Even his
voice was gentle. He'd had the quinsy and swollen glands when he
was young, he told me, and it had left him with a weak throat,
and a hesitating, whispering fashion of speech. He was always
well dressed, very neat and plain, but his eyes were weak, just
as mine are, and he wore tinted glasses against the glare."
"Well, and what happened when Mr. Windibank, your stepfather,
returned to France?"
"Mr. Hosmer Angel came to the house again and proposed that we
should marry before father came back. He was in dreadful earnest
and made me swear, with my hands on the Testament, that whatever
happened I would always be true to him. Mother said he was quite
right to make me swear, and that it was a sign of his passion.
Mother was all in his favour from the first and was even fonder
of him than I was. Then, when they talked of marrying within the
week, I began to ask about father; but they both said never to
mind about father, but just to tell him afterwards, and mother
said she would make it all right with him. I didn't quite like
that, Mr. Holmes. It seemed funny that I should ask his leave, as
he was only a few years older than me; but I didn't want to do
anything on the sly, so I wrote to father at Bordeaux, where the
company has its French offices, but the letter came back to me on
the very morning of the wedding."
"It missed him, then?"
"Yes, sir; for he had started to England just before it arrived."
"Ha! that was unfortunate. Your wedding was arranged, then, for
the Friday. Was it to be in church?"
"Yes, sir, but very quietly. It was to be at St. Saviour's, near
King's Cross, and we were to have breakfast afterwards at the St.
Pancras Hotel. Hosmer came for us in a hansom, but as there were
two of us he put us both into it and stepped himself into a
four-wheeler, which happened to be the only other cab in the
street. We got to the church first, and when the four-wheeler
drove up we waited for him to step out, but he never did, and
when the cabman got down from the box and looked there was no one
there! The cabman said that he could not imagine what had become
of him, for he had seen him get in with his own eyes. That was
last Friday, Mr. Holmes, and I have never seen or heard anything
since then to throw any light upon what became of him."
"It seems to me that you have been very shamefully treated," said
Holmes.
"Oh, no, sir! He was too good and kind to leave me so. Why, all
the morning he was saying to me that, whatever happened, I was to
be true; and that even if something quite unforeseen occurred to
separate us, I was always to remember that I was pledged to him,
and that he would claim his pledge sooner or later. It seemed
strange talk for a wedding-morning, but what has happened since
gives a meaning to it."
"Most certainly it does. Your own opinion is, then, that some
unforeseen catastrophe has occurred to him?"
"Yes, sir. I believe that he foresaw some danger, or else he
would not have talked so. And then I think that what he foresaw
happened."
"But you have no notion as to what it could have been?"
"None."
"One more question. How did your mother take the matter?"
"She was angry, and said that I was never to speak of the matter
again."
"And your father? Did you tell him?"
"Yes; and he seemed to think, with me, that something had
happened, and that I should hear of Hosmer again. As he said,
what interest could anyone have in bringing me to the doors of
the church, and then leaving me? Now, if he had borrowed my
money, or if he had married me and got my money settled on him,
there might be some reason, but Hosmer was very independent about
money and never would look at a shilling of mine. And yet, what
could have happened? And why could he not write? Oh, it drives me
half-mad to think of it, and I can't sleep a wink at night." She
pulled a little handkerchief out of her muff and began to sob
heavily into it.
"I shall glance into the case for you," said Holmes, rising, "and
I have no doubt that we shall reach some definite result. Let the
weight of the matter rest upon me now, and do not let your mind
dwell upon it further. Above all, try to let Mr. Hosmer Angel
vanish from your memory, as he has done from your life."
"Then you don't think I'll see him again?"
"I fear not."
"Then what has happened to him?"
"You will leave that question in my hands. I should like an
accurate description of him and any letters of his which you can
spare."
"I advertised for him in last Saturday's Chronicle," said she.
"Here is the slip and here are four letters from him."
"Thank you. And your address?"
"No. 31 Lyon Place, Camberwell."
"Mr. Angel's address you never had, I understand. Where is your
father's place of business?"
"He travels for Westhouse & Marbank, the great claret importers
of Fenchurch Street."
"Thank you. You have made your statement very clearly. You will
leave the papers here, and remember the advice which I have given
you. Let the whole incident be a sealed book, and do not allow it
to affect your life."
"You are very kind, Mr. Holmes, but I cannot do that. I shall be
true to Hosmer. He shall find me ready when he comes back."
For all the preposterous hat and the vacuous face, there was
something noble in the simple faith of our visitor which
compelled our respect. She laid her little bundle of papers upon
the table and went her way, with a promise to come again whenever
she might be summoned.
Sherlock Holmes sat silent for a few minutes with his fingertips
still pressed together, his legs stretched out in front of him,
and his gaze directed upward to the ceiling. Then he took down
from the rack the old and oily clay pipe, which was to him as a
counsellor, and, having lit it, he leaned back in his chair, with
the thick blue cloud-wreaths spinning up from him, and a look of
infinite languor in his face.
"Quite an interesting study, that maiden," he observed. "I found
her more interesting than her little problem, which, by the way,
is rather a trite one. You will find parallel cases, if you
consult my index, in Andover in '77, and there was something of
the sort at The Hague last year. Old as is the idea, however,
there were one or two details which were new to me. But the
maiden herself was most instructive."
"You appeared to read a good deal upon her which was quite
invisible to me," I remarked.
"Not invisible but unnoticed, Watson. You did not know where to
look, and so you missed all that was important. I can never bring
you to realise the importance of sleeves, the suggestiveness of
thumb-nails, or the great issues that may hang from a boot-lace.
Now, what did you gather from that woman's appearance? Describe
it."
"Well, she had a slate-coloured, broad-brimmed straw hat, with a
feather of a brickish red. Her jacket was black, with black beads
sewn upon it, and a fringe of little black jet ornaments. Her
dress was brown, rather darker than coffee colour, with a little
purple plush at the neck and sleeves. Her gloves were greyish and
were worn through at the right forefinger. Her boots I didn't
observe. She had small round, hanging gold earrings, and a
general air of being fairly well-to-do in a vulgar, comfortable,
easy-going way."
Sherlock Holmes clapped his hands softly together and chuckled.
"'Pon my word, Watson, you are coming along wonderfully. You have
really done very well indeed. It is true that you have missed
everything of importance, but you have hit upon the method, and
you have a quick eye for colour. Never trust to general
impressions, my boy, but concentrate yourself upon details. My
first glance is always at a woman's sleeve. In a man it is
perhaps better first to take the knee of the trouser. As you
observe, this woman had plush upon her sleeves, which is a most
useful material for showing traces. The double line a little
above the wrist, where the typewritist presses against the table,
was beautifully defined. The sewing-machine, of the hand type,
leaves a similar mark, but only on the left arm, and on the side
of it farthest from the thumb, instead of being right across the
broadest part, as this was. I then glanced at her face, and,
observing the dint of a pince-nez at either side of her nose, I
ventured a remark upon short sight and typewriting, which seemed
to surprise her."
"It surprised me."
"But, surely, it was obvious. I was then much surprised and
interested on glancing down to observe that, though the boots
which she was wearing were not unlike each other, they were
really odd ones; the one having a slightly decorated toe-cap, and
the other a plain one. One was buttoned only in the two lower
buttons out of five, and the other at the first, third, and
fifth. Now, when you see that a young lady, otherwise neatly
dressed, has come away from home with odd boots, half-buttoned,
it is no great deduction to say that she came away in a hurry."
"And what else?" I asked, keenly interested, as I always was, by
my friend's incisive reasoning.
"I noted, in passing, that she had written a note before leaving
home but after being fully dressed. You observed that her right
glove was torn at the forefinger, but you did not apparently see
that both glove and finger were stained with violet ink. She had
written in a hurry and dipped her pen too deep. It must have been
this morning, or the mark would not remain clear upon the finger.
All this is amusing, though rather elementary, but I must go back
to business, Watson. Would you mind reading me the advertised
description of Mr. Hosmer Angel?"
I held the little printed slip to the light.
"Missing," it said, "on the morning of the fourteenth, a gentleman
named Hosmer Angel. About five ft. seven in. in height;
strongly built, sallow complexion, black hair, a little bald in
the centre, bushy, black side-whiskers and moustache; tinted
glasses, slight infirmity of speech. Was dressed, when last seen,
in black frock-coat faced with silk, black waistcoat, gold Albert
chain, and grey Harris tweed trousers, with brown gaiters over
elastic-sided boots. Known to have been employed in an office in
Leadenhall Street. Anybody bringing--"
"That will do," said Holmes. "As to the letters," he continued,
glancing over them, "they are very commonplace. Absolutely no
clue in them to Mr. Angel, save that he quotes Balzac once. There
is one remarkable point, however, which will no doubt strike
you."
"They are typewritten," I remarked.
"Not only that, but the signature is typewritten. Look at the
neat little 'Hosmer Angel' at the bottom. There is a date, you
see, but no superscription except Leadenhall Street, which is
rather vague. The point about the signature is very suggestive--in
fact, we may call it conclusive."
"Of what?"
"My dear fellow, is it possible you do not see how strongly it
bears upon the case?"
"I cannot say that I do unless it were that he wished to be able
to deny his signature if an action for breach of promise were
instituted."
"No, that was not the point. However, I shall write two letters,
which should settle the matter. One is to a firm in the City, the
other is to the young lady's stepfather, Mr. Windibank, asking
him whether he could meet us here at six o'clock tomorrow
evening. It is just as well that we should do business with the
male relatives. And now, Doctor, we can do nothing until the
answers to those letters come, so we may put our little problem
upon the shelf for the interim."
I had had so many reasons to believe in my friend's subtle powers
of reasoning and extraordinary energy in action that I felt that
he must have some solid grounds for the assured and easy
demeanour with which he treated the singular mystery which he had
been called upon to fathom. Once only had I known him to fail, in
the case of the King of Bohemia and of the Irene Adler
photograph; but when I looked back to the weird business of the
Sign of Four, and the extraordinary circumstances connected with
the Study in Scarlet, I felt that it would be a strange tangle
indeed which he could not unravel.
I left him then, still puffing at his black clay pipe, with the
conviction that when I came again on the next evening I would
find that he held in his hands all the clues which would lead up
to the identity of the disappearing bridegroom of Miss Mary
Sutherland.
A professional case of great gravity was engaging my own
attention at the time, and the whole of next day I was busy at
the bedside of the sufferer. It was not until close upon six
o'clock that I found myself free and was able to spring into a
hansom and drive to Baker Street, half afraid that I might be too
late to assist at the dénouement of the little mystery. I found
Sherlock Holmes alone, however, half asleep, with his long, thin
form curled up in the recesses of his armchair. A formidable
array of bottles and test-tubes, with the pungent cleanly smell
of hydrochloric acid, told me that he had spent his day in the
chemical work which was so dear to him.
"Well, have you solved it?" I asked as I entered.
"Yes. It was the bisulphate of baryta."
"No, no, the mystery!" I cried.
"Oh, that! I thought of the salt that I have been working upon.
There was never any mystery in the matter, though, as I said
yesterday, some of the details are of interest. The only drawback
is that there is no law, I fear, that can touch the scoundrel."
"Who was he, then, and what was his object in deserting Miss
Sutherland?"
The question was hardly out of my mouth, and Holmes had not yet
opened his lips to reply, when we heard a heavy footfall in the
passage and a tap at the door.
"This is the girl's stepfather, Mr. James Windibank," said
Holmes. "He has written to me to say that he would be here at
six. Come in!"
The man who entered was a sturdy, middle-sized fellow, some
thirty years of age, clean-shaven, and sallow-skinned, with a
bland, insinuating manner, and a pair of wonderfully sharp and
penetrating grey eyes. He shot a questioning glance at each of
us, placed his shiny top-hat upon the sideboard, and with a
slight bow sidled down into the nearest chair.
"Good-evening, Mr. James Windibank," said Holmes. "I think that
this typewritten letter is from you, in which you made an
appointment with me for six o'clock?"
"Yes, sir. I am afraid that I am a little late, but I am not
quite my own master, you know. I am sorry that Miss Sutherland
has troubled you about this little matter, for I think it is far
better not to wash linen of the sort in public. It was quite
against my wishes that she came, but she is a very excitable,
impulsive girl, as you may have noticed, and she is not easily
controlled when she has made up her mind on a point. Of course, I
did not mind you so much, as you are not connected with the
official police, but it is not pleasant to have a family
misfortune like this noised abroad. Besides, it is a useless
expense, for how could you possibly find this Hosmer Angel?"
"On the contrary," said Holmes quietly; "I have every reason to
believe that I will succeed in discovering Mr. Hosmer Angel."
Mr. Windibank gave a violent start and dropped his gloves. "I am
delighted to hear it," he said.
"It is a curious thing," remarked Holmes, "that a typewriter has
really quite as much individuality as a man's handwriting. Unless
they are quite new, no two of them write exactly alike. Some
letters get more worn than others, and some wear only on one
side. Now, you remark in this note of yours, Mr. Windibank, that
in every case there is some little slurring over of the 'e,' and
a slight defect in the tail of the 'r.' There are fourteen other
characteristics, but those are the more obvious."
"We do all our correspondence with this machine at the office,
and no doubt it is a little worn," our visitor answered, glancing
keenly at Holmes with his bright little eyes.
"And now I will show you what is really a very interesting study,
Mr. Windibank," Holmes continued. "I think of writing another
little monograph some of these days on the typewriter and its
relation to crime. It is a subject to which I have devoted some
little attention. I have here four letters which purport to come
from the missing man. They are all typewritten. In each case, not
only are the 'e's' slurred and the 'r's' tailless, but you will
observe, if you care to use my magnifying lens, that the fourteen
other characteristics to which I have alluded are there as well."
Mr. Windibank sprang out of his chair and picked up his hat. "I
cannot waste time over this sort of fantastic talk, Mr. Holmes,"
he said. "If you can catch the man, catch him, and let me know
when you have done it."
"Certainly," said Holmes, stepping over and turning the key in
the door. "I let you know, then, that I have caught him!"
"What! where?" shouted Mr. Windibank, turning white to his lips
and glancing about him like a rat in a trap.
"Oh, it won't do--really it won't," said Holmes suavely. "There
is no possible getting out of it, Mr. Windibank. It is quite too
transparent, and it was a very bad compliment when you said that
it was impossible for me to solve so simple a question. That's
right! Sit down and let us talk it over."
Our visitor collapsed into a chair, with a ghastly face and a
glitter of moisture on his brow. "It--it's not actionable," he
stammered.
"I am very much afraid that it is not. But between ourselves,
Windibank, it was as cruel and selfish and heartless a trick in a
petty way as ever came before me. Now, let me just run over the
course of events, and you will contradict me if I go wrong."
The man sat huddled up in his chair, with his head sunk upon his
breast, like one who is utterly crushed. Holmes stuck his feet up
on the corner of the mantelpiece and, leaning back with his hands
in his pockets, began talking, rather to himself, as it seemed,
than to us.
"The man married a woman very much older than himself for her
money," said he, "and he enjoyed the use of the money of the
daughter as long as she lived with them. It was a considerable
sum, for people in their position, and the loss of it would have
made a serious difference. It was worth an effort to preserve it.
The daughter was of a good, amiable disposition, but affectionate
and warm-hearted in her ways, so that it was evident that with
her fair personal advantages, and her little income, she would
not be allowed to remain single long. Now her marriage would
mean, of course, the loss of a hundred a year, so what does her
stepfather do to prevent it? He takes the obvious course of
keeping her at home and forbidding her to seek the company of
people of her own age. But soon he found that that would not
answer forever. She became restive, insisted upon her rights, and
finally announced her positive intention of going to a certain
ball. What does her clever stepfather do then? He conceives an
idea more creditable to his head than to his heart. With the
connivance and assistance of his wife he disguised himself,
covered those keen eyes with tinted glasses, masked the face with
a moustache and a pair of bushy whiskers, sunk that clear voice
into an insinuating whisper, and doubly secure on account of the
girl's short sight, he appears as Mr. Hosmer Angel, and keeps off
other lovers by making love himself."
"It was only a joke at first," groaned our visitor. "We never
thought that she would have been so carried away."
"Very likely not. However that may be, the young lady was very
decidedly carried away, and, having quite made up her mind that
her stepfather was in France, the suspicion of treachery never
for an instant entered her mind. She was flattered by the
gentleman's attentions, and the effect was increased by the
loudly expressed admiration of her mother. Then Mr. Angel began
to call, for it was obvious that the matter should be pushed as
far as it would go if a real effect were to be produced. There
were meetings, and an engagement, which would finally secure the
girl's affections from turning towards anyone else. But the
deception could not be kept up forever. These pretended journeys
to France were rather cumbrous. The thing to do was clearly to
bring the business to an end in such a dramatic manner that it
would leave a permanent impression upon the young lady's mind and
prevent her from looking upon any other suitor for some time to
come. Hence those vows of fidelity exacted upon a Testament, and
hence also the allusions to a possibility of something happening
on the very morning of the wedding. James Windibank wished Miss
Sutherland to be so bound to Hosmer Angel, and so uncertain as to
his fate, that for ten years to come, at any rate, she would not
listen to another man. As far as the church door he brought her,
and then, as he could go no farther, he conveniently vanished
away by the old trick of stepping in at one door of a
four-wheeler and out at the other. I think that was the chain of
events, Mr. Windibank!"
Our visitor had recovered something of his assurance while Holmes
had been talking, and he rose from his chair now with a cold
sneer upon his pale face.
"It may be so, or it may not, Mr. Holmes," said he, "but if you
are so very sharp you ought to be sharp enough to know that it is
you who are breaking the law now, and not me. I have done nothing
actionable from the first, but as long as you keep that door
locked you lay yourself open to an action for assault and illegal
constraint."
"The law cannot, as you say, touch you," said Holmes, unlocking
and throwing open the door, "yet there never was a man who
deserved punishment more. If the young lady has a brother or a
friend, he ought to lay a whip across your shoulders. By Jove!"
he continued, flushing up at the sight of the bitter sneer upon
the man's face, "it is not part of my duties to my client, but
here's a hunting crop handy, and I think I shall just treat
myself to--" He took two swift steps to the whip, but before he
could grasp it there was a wild clatter of steps upon the stairs,
the heavy hall door banged, and from the window we could see Mr.
James Windibank running at the top of his speed down the road.
"There's a cold-blooded scoundrel!" said Holmes, laughing, as he
threw himself down into his chair once more. "That fellow will
rise from crime to crime until he does something very bad, and
ends on a gallows. The case has, in some respects, been not
entirely devoid of interest."
"I cannot now entirely see all the steps of your reasoning," I
remarked.
"Well, of course it was obvious from the first that this Mr.
Hosmer Angel must have some strong object for his curious
conduct, and it was equally clear that the only man who really
profited by the incident, as far as we could see, was the
stepfather. Then the fact that the two men were never together,
but that the one always appeared when the other was away, was
suggestive. So were the tinted spectacles and the curious voice,
which both hinted at a disguise, as did the bushy whiskers. My
suspicions were all confirmed by his peculiar action in
typewriting his signature, which, of course, inferred that his
handwriting was so familiar to her that she would recognise even
the smallest sample of it. You see all these isolated facts,
together with many minor ones, all pointed in the same
direction."
"And how did you verify them?"
"Having once spotted my man, it was easy to get corroboration. I
knew the firm for which this man worked. Having taken the printed
description. I eliminated everything from it which could be the
result of a disguise--the whiskers, the glasses, the voice, and I
sent it to the firm, with a request that they would inform me
whether it answered to the description of any of their
travellers. I had already noticed the peculiarities of the
typewriter, and I wrote to the man himself at his business
address asking him if he would come here. As I expected, his
reply was typewritten and revealed the same trivial but
characteristic defects. The same post brought me a letter from
Westhouse & Marbank, of Fenchurch Street, to say that the
description tallied in every respect with that of their employé,
James Windibank. Voilà tout!"
"And Miss Sutherland?"
"If I tell her she will not believe me. You may remember the old
Persian saying, 'There is danger for him who taketh the tiger
cub, and danger also for whoso snatches a delusion from a woman.'
There is as much sense in Hafiz as in Horace, and as much
knowledge of the world."
ADVENTURE IV. THE BOSCOMBE VALLEY MYSTERY
We were seated at breakfast one morning, my wife and I, when the
maid brought in a telegram. It was from Sherlock Holmes and ran
in this way:
"Have you a couple of days to spare? Have just been wired for from
the west of England in connection with Boscombe Valley tragedy.
Shall be glad if you will come with me. Air and scenery perfect.
Leave Paddington by the 11:15."
"What do you say, dear?" said my wife, looking across at me.
"Will you go?"
"I really don't know what to say. I have a fairly long list at
present."
"Oh, Anstruther would do your work for you. You have been looking
a little pale lately. I think that the change would do you good,
and you are always so interested in Mr. Sherlock Holmes' cases."
"I should be ungrateful if I were not, seeing what I gained
through one of them," I answered. "But if I am to go, I must pack
at once, for I have only half an hour."
My experience of camp life in Afghanistan had at least had the
effect of making me a prompt and ready traveller. My wants were
few and simple, so that in less than the time stated I was in a
cab with my valise, rattling away to Paddington Station. Sherlock
Holmes was pacing up and down the platform, his tall, gaunt
figure made even gaunter and taller by his long grey
travelling-cloak and close-fitting cloth cap.
"It is really very good of you to come, Watson," said he. "It
makes a considerable difference to me, having someone with me on
whom I can thoroughly rely. Local aid is always either worthless
or else biassed. If you will keep the two corner seats I shall
get the tickets."
We had the carriage to ourselves save for an immense litter of
papers which Holmes had brought with him. Among these he rummaged
and read, with intervals of note-taking and of meditation, until
we were past Reading. Then he suddenly rolled them all into a
gigantic ball and tossed them up onto the rack.
"Have you heard anything of the case?" he asked.
"Not a word. I have not seen a paper for some days."
"The London press has not had very full accounts. I have just
been looking through all the recent papers in order to master the
particulars. It seems, from what I gather, to be one of those
simple cases which are so extremely difficult."
"That sounds a little paradoxical."
"But it is profoundly true. Singularity is almost invariably a
clue. The more featureless and commonplace a crime is, the more
difficult it is to bring it home. In this case, however, they
have established a very serious case against the son of the
murdered man."
"It is a murder, then?"
"Well, it is conjectured to be so. I shall take nothing for
granted until I have the opportunity of looking personally into
it. I will explain the state of things to you, as far as I have
been able to understand it, in a very few words.
"Boscombe Valley is a country district not very far from Ross, in
Herefordshire. The largest landed proprietor in that part is a
Mr. John Turner, who made his money in Australia and returned
some years ago to the old country. One of the farms which he
held, that of Hatherley, was let to Mr. Charles McCarthy, who was
also an ex-Australian. The men had known each other in the
colonies, so that it was not unnatural that when they came to
settle down they should do so as near each other as possible.
Turner was apparently the richer man, so McCarthy became his
tenant but still remained, it seems, upon terms of perfect
equality, as they were frequently together. McCarthy had one son,
a lad of eighteen, and Turner had an only daughter of the same
age, but neither of them had wives living. They appear to have
avoided the society of the neighbouring English families and to
have led retired lives, though both the McCarthys were fond of
sport and were frequently seen at the race-meetings of the
neighbourhood. McCarthy kept two servants--a man and a girl.
Turner had a considerable household, some half-dozen at the
least. That is as much as I have been able to gather about the
families. Now for the facts.
"On June 3rd, that is, on Monday last, McCarthy left his house at
Hatherley about three in the afternoon and walked down to the
Boscombe Pool, which is a small lake formed by the spreading out
of the stream which runs down the Boscombe Valley. He had been
out with his serving-man in the morning at Ross, and he had told
the man that he must hurry, as he had an appointment of
importance to keep at three. From that appointment he never came
back alive.
"From Hatherley Farm-house to the Boscombe Pool is a quarter of a
mile, and two people saw him as he passed over this ground. One
was an old woman, whose name is not mentioned, and the other was
William Crowder, a game-keeper in the employ of Mr. Turner. Both
these witnesses depose that Mr. McCarthy was walking alone. The
game-keeper adds that within a few minutes of his seeing Mr.
McCarthy pass he had seen his son, Mr. James McCarthy, going the
same way with a gun under his arm. To the best of his belief, the
father was actually in sight at the time, and the son was
following him. He thought no more of the matter until he heard in
the evening of the tragedy that had occurred.
"The two McCarthys were seen after the time when William Crowder,
the game-keeper, lost sight of them. The Boscombe Pool is thickly
wooded round, with just a fringe of grass and of reeds round the
edge. A girl of fourteen, Patience Moran, who is the daughter of
the lodge-keeper of the Boscombe Valley estate, was in one of the
woods picking flowers. She states that while she was there she
saw, at the border of the wood and close by the lake, Mr.
McCarthy and his son, and that they appeared to be having a
violent quarrel. She heard Mr. McCarthy the elder using very
strong language to his son, and she saw the latter raise up his
hand as if to strike his father. She was so frightened by their
violence that she ran away and told her mother when she reached
home that she had left the two McCarthys quarrelling near
Boscombe Pool, and that she was afraid that they were going to
fight. She had hardly said the words when young Mr. McCarthy came
running up to the lodge to say that he had found his father dead
in the wood, and to ask for the help of the lodge-keeper. He was
much excited, without either his gun or his hat, and his right
hand and sleeve were observed to be stained with fresh blood. On
following him they found the dead body stretched out upon the
grass beside the pool. The head had been beaten in by repeated
blows of some heavy and blunt weapon. The injuries were such as
might very well have been inflicted by the butt-end of his son's
gun, which was found lying on the grass within a few paces of the
body. Under these circumstances the young man was instantly
arrested, and a verdict of 'wilful murder' having been returned
at the inquest on Tuesday, he was on Wednesday brought before the
magistrates at Ross, who have referred the case to the next
Assizes. Those are the main facts of the case as they came out
before the coroner and the police-court."
"I could hardly imagine a more damning case," I remarked. "If
ever circumstantial evidence pointed to a criminal it does so
here."
"Circumstantial evidence is a very tricky thing," answered Holmes
thoughtfully. "It may seem to point very straight to one thing,
but if you shift your own point of view a little, you may find it
pointing in an equally uncompromising manner to something
entirely different. It must be confessed, however, that the case
looks exceedingly grave against the young man, and it is very
possible that he is indeed the culprit. There are several people
in the neighbourhood, however, and among them Miss Turner, the
daughter of the neighbouring landowner, who believe in his
innocence, and who have retained Lestrade, whom you may recollect
in connection with the Study in Scarlet, to work out the case in
his interest. Lestrade, being rather puzzled, has referred the
case to me, and hence it is that two middle-aged gentlemen are
flying westward at fifty miles an hour instead of quietly
digesting their breakfasts at home."
"I am afraid," said I, "that the facts are so obvious that you
will find little credit to be gained out of this case."
"There is nothing more deceptive than an obvious fact," he
answered, laughing. "Besides, we may chance to hit upon some
other obvious facts which may have been by no means obvious to
Mr. Lestrade. You know me too well to think that I am boasting
when I say that I shall either confirm or destroy his theory by
means which he is quite incapable of employing, or even of
understanding. To take the first example to hand, I very clearly
perceive that in your bedroom the window is upon the right-hand
side, and yet I question whether Mr. Lestrade would have noted
even so self-evident a thing as that."
"How on earth--"
"My dear fellow, I know you well. I know the military neatness
which characterises you. You shave every morning, and in this
season you shave by the sunlight; but since your shaving is less
and less complete as we get farther back on the left side, until
it becomes positively slovenly as we get round the angle of the
jaw, it is surely very clear that that side is less illuminated
than the other. I could not imagine a man of your habits looking
at himself in an equal light and being satisfied with such a
result. I only quote this as a trivial example of observation and
inference. Therein lies my métier, and it is just possible that
it may be of some service in the investigation which lies before
us. There are one or two minor points which were brought out in
the inquest, and which are worth considering."
"What are they?"
"It appears that his arrest did not take place at once, but after
the return to Hatherley Farm. On the inspector of constabulary
informing him that he was a prisoner, he remarked that he was not
surprised to hear it, and that it was no more than his deserts.
This observation of his had the natural effect of removing any
traces of doubt which might have remained in the minds of the
coroner's jury."
"It was a confession," I ejaculated.
"No, for it was followed by a protestation of innocence."
"Coming on the top of such a damning series of events, it was at
least a most suspicious remark."
"On the contrary," said Holmes, "it is the brightest rift which I
can at present see in the clouds. However innocent he might be,
he could not be such an absolute imbecile as not to see that the
circumstances were very black against him. Had he appeared
surprised at his own arrest, or feigned indignation at it, I
should have looked upon it as highly suspicious, because such
surprise or anger would not be natural under the circumstances,
and yet might appear to be the best policy to a scheming man. His
frank acceptance of the situation marks him as either an innocent
man, or else as a man of considerable self-restraint and
firmness. As to his remark about his deserts, it was also not
unnatural if you consider that he stood beside the dead body of
his father, and that there is no doubt that he had that very day
so far forgotten his filial duty as to bandy words with him, and
even, according to the little girl whose evidence is so
important, to raise his hand as if to strike him. The
self-reproach and contrition which are displayed in his remark
appear to me to be the signs of a healthy mind rather than of a
guilty one."
I shook my head. "Many men have been hanged on far slighter
evidence," I remarked.
"So they have. And many men have been wrongfully hanged."
"What is the young man's own account of the matter?"
"It is, I am afraid, not very encouraging to his supporters,
though there are one or two points in it which are suggestive.
You will find it here, and may read it for yourself."
He picked out from his bundle a copy of the local Herefordshire
paper, and having turned down the sheet he pointed out the
paragraph in which the unfortunate young man had given his own
statement of what had occurred. I settled myself down in the
corner of the carriage and read it very carefully. It ran in this
way:
"Mr. James McCarthy, the only son of the deceased, was then called
and gave evidence as follows: 'I had been away from home for
three days at Bristol, and had only just returned upon the
morning of last Monday, the 3rd. My father was absent from home at
the time of my arrival, and I was informed by the maid that he
had driven over to Ross with John Cobb, the groom. Shortly after
my return I heard the wheels of his trap in the yard, and,
looking out of my window, I saw him get out and walk rapidly out
of the yard, though I was not aware in which direction he was
going. I then took my gun and strolled out in the direction of
the Boscombe Pool, with the intention of visiting the rabbit
warren which is upon the other side. On my way I saw William
Crowder, the game-keeper, as he had stated in his evidence; but
he is mistaken in thinking that I was following my father. I had
no idea that he was in front of me. When about a hundred yards
from the pool I heard a cry of "Cooee!" which was a usual signal
between my father and myself. I then hurried forward, and found
him standing by the pool. He appeared to be much surprised at
seeing me and asked me rather roughly what I was doing there. A
conversation ensued which led to high words and almost to blows,
for my father was a man of a very violent temper. Seeing that his
passion was becoming ungovernable, I left him and returned
towards Hatherley Farm. I had not gone more than 150 yards,
however, when I heard a hideous outcry behind me, which caused me
to run back again. I found my father expiring upon the ground,
with his head terribly injured. I dropped my gun and held him in
my arms, but he almost instantly expired. I knelt beside him for
some minutes, and then made my way to Mr. Turner's lodge-keeper,
his house being the nearest, to ask for assistance. I saw no one
near my father when I returned, and I have no idea how he came by
his injuries. He was not a popular man, being somewhat cold and
forbidding in his manners, but he had, as far as I know, no
active enemies. I know nothing further of the matter.'
"The Coroner: Did your father make any statement to you before
he died?
"Witness: He mumbled a few words, but I could only catch some
allusion to a rat.
"The Coroner: What did you understand by that?
"Witness: It conveyed no meaning to me. I thought that he was
delirious.
"The Coroner: What was the point upon which you and your father
had this final quarrel?
"Witness: I should prefer not to answer.
"The Coroner: I am afraid that I must press it.
"Witness: It is really impossible for me to tell you. I can
assure you that it has nothing to do with the sad tragedy which
followed.
"The Coroner: That is for the court to decide. I need not point
out to you that your refusal to answer will prejudice your case
considerably in any future proceedings which may arise.
"Witness: I must still refuse.
"The Coroner: I understand that the cry of 'Cooee' was a common
signal between you and your father?
"Witness: It was.
"The Coroner: How was it, then, that he uttered it before he saw
you, and before he even knew that you had returned from Bristol?
"Witness (with considerable confusion): I do not know.
"A Juryman: Did you see nothing which aroused your suspicions
when you returned on hearing the cry and found your father
fatally injured?
"Witness: Nothing definite.
"The Coroner: What do you mean?
"Witness: I was so disturbed and excited as I rushed out into
the open, that I could think of nothing except of my father. Yet
I have a vague impression that as I ran forward something lay
upon the ground to the left of me. It seemed to me to be
something grey in colour, a coat of some sort, or a plaid perhaps.
When I rose from my father I looked round for it, but it was
gone.
"'Do you mean that it disappeared before you went for help?'
"'Yes, it was gone.'
"'You cannot say what it was?'
"'No, I had a feeling something was there.'
"'How far from the body?'
"'A dozen yards or so.'
"'And how far from the edge of the wood?'
"'About the same.'
"'Then if it was removed it was while you were within a dozen
yards of it?'
"'Yes, but with my back towards it.'
"This concluded the examination of the witness."
"I see," said I as I glanced down the column, "that the coroner
in his concluding remarks was rather severe upon young McCarthy.
He calls attention, and with reason, to the discrepancy about his
father having signalled to him before seeing him, also to his
refusal to give details of his conversation with his father, and
his singular account of his father's dying words. They are all,
as he remarks, very much against the son."
Holmes laughed softly to himself and stretched himself out upon
the cushioned seat. "Both you and the coroner have been at some
pains," said he, "to single out the very strongest points in the
young man's favour. Don't you see that you alternately give him
credit for having too much imagination and too little? Too
little, if he could not invent a cause of quarrel which would
give him the sympathy of the jury; too much, if he evolved from
his own inner consciousness anything so outré as a dying
reference to a rat, and the incident of the vanishing cloth. No,
sir, I shall approach this case from the point of view that what
this young man says is true, and we shall see whither that
hypothesis will lead us. And now here is my pocket Petrarch, and
not another word shall I say of this case until we are on the
scene of action. We lunch at Swindon, and I see that we shall be
there in twenty minutes."
It was nearly four o'clock when we at last, after passing through
the beautiful Stroud Valley, and over the broad gleaming Severn,
found ourselves at the pretty little country-town of Ross. A
lean, ferret-like man, furtive and sly-looking, was waiting for
us upon the platform. In spite of the light brown dustcoat and
leather-leggings which he wore in deference to his rustic
surroundings, I had no difficulty in recognising Lestrade, of
Scotland Yard. With him we drove to the Hereford Arms where a
room had already been engaged for us.
"I have ordered a carriage," said Lestrade as we sat over a cup
of tea. "I knew your energetic nature, and that you would not be
happy until you had been on the scene of the crime."
"It was very nice and complimentary of you," Holmes answered. "It
is entirely a question of barometric pressure."
Lestrade looked startled. "I do not quite follow," he said.
"How is the glass? Twenty-nine, I see. No wind, and not a cloud
in the sky. I have a caseful of cigarettes here which need
smoking, and the sofa is very much superior to the usual country
hotel abomination. I do not think that it is probable that I
shall use the carriage to-night."
Lestrade laughed indulgently. "You have, no doubt, already formed
your conclusions from the newspapers," he said. "The case is as
plain as a pikestaff, and the more one goes into it the plainer
it becomes. Still, of course, one can't refuse a lady, and such a
very positive one, too. She has heard of you, and would have your
opinion, though I repeatedly told her that there was nothing
which you could do which I had not already done. Why, bless my
soul! here is her carriage at the door."
He had hardly spoken before there rushed into the room one of the
most lovely young women that I have ever seen in my life. Her
violet eyes shining, her lips parted, a pink flush upon her
cheeks, all thought of her natural reserve lost in her
overpowering excitement and concern.
"Oh, Mr. Sherlock Holmes!" she cried, glancing from one to the
other of us, and finally, with a woman's quick intuition,
fastening upon my companion, "I am so glad that you have come. I
have driven down to tell you so. I know that James didn't do it.
I know it, and I want you to start upon your work knowing it,
too. Never let yourself doubt upon that point. We have known each
other since we were little children, and I know his faults as no
one else does; but he is too tender-hearted to hurt a fly. Such a
charge is absurd to anyone who really knows him."
"I hope we may clear him, Miss Turner," said Sherlock Holmes.
"You may rely upon my doing all that I can."
"But you have read the evidence. You have formed some conclusion?
Do you not see some loophole, some flaw? Do you not yourself
think that he is innocent?"
"I think that it is very probable."
"There, now!" she cried, throwing back her head and looking
defiantly at Lestrade. "You hear! He gives me hopes."
Lestrade shrugged his shoulders. "I am afraid that my colleague
has been a little quick in forming his conclusions," he said.
"But he is right. Oh! I know that he is right. James never did
it. And about his quarrel with his father, I am sure that the
reason why he would not speak about it to the coroner was because
I was concerned in it."
"In what way?" asked Holmes.
"It is no time for me to hide anything. James and his father had
many disagreements about me. Mr. McCarthy was very anxious that
there should be a marriage between us. James and I have always
loved each other as brother and sister; but of course he is young
and has seen very little of life yet, and--and--well, he
naturally did not wish to do anything like that yet. So there
were quarrels, and this, I am sure, was one of them."
"And your father?" asked Holmes. "Was he in favour of such a
union?"
"No, he was averse to it also. No one but Mr. McCarthy was in
favour of it." A quick blush passed over her fresh young face as
Holmes shot one of his keen, questioning glances at her.
"Thank you for this information," said he. "May I see your father
if I call to-morrow?"
"I am afraid the doctor won't allow it."
"The doctor?"
"Yes, have you not heard? Poor father has never been strong for
years back, but this has broken him down completely. He has taken
to his bed, and Dr. Willows says that he is a wreck and that his
nervous system is shattered. Mr. McCarthy was the only man alive
who had known dad in the old days in Victoria."
"Ha! In Victoria! That is important."
"Yes, at the mines."
"Quite so; at the gold-mines, where, as I understand, Mr. Turner
made his money."
"Yes, certainly."
"Thank you, Miss Turner. You have been of material assistance to
me."
"You will tell me if you have any news to-morrow. No doubt you
will go to the prison to see James. Oh, if you do, Mr. Holmes, do
tell him that I know him to be innocent."
"I will, Miss Turner."
"I must go home now, for dad is very ill, and he misses me so if
I leave him. Good-bye, and God help you in your undertaking." She
hurried from the room as impulsively as she had entered, and we
heard the wheels of her carriage rattle off down the street.
"I am ashamed of you, Holmes," said Lestrade with dignity after a
few minutes' silence. "Why should you raise up hopes which you
are bound to disappoint? I am not over-tender of heart, but I
call it cruel."
"I think that I see my way to clearing James McCarthy," said
Holmes. "Have you an order to see him in prison?"
"Yes, but only for you and me."
"Then I shall reconsider my resolution about going out. We have
still time to take a train to Hereford and see him to-night?"
"Ample."
"Then let us do so. Watson, I fear that you will find it very
slow, but I shall only be away a couple of hours."
I walked down to the station with them, and then wandered through
the streets of the little town, finally returning to the hotel,
where I lay upon the sofa and tried to interest myself in a
yellow-backed novel. The puny plot of the story was so thin,
however, when compared to the deep mystery through which we were
groping, and I found my attention wander so continually from the
action to the fact, that I at last flung it across the room and
gave myself up entirely to a consideration of the events of the
day. Supposing that this unhappy young man's story were
absolutely true, then what hellish thing, what absolutely
unforeseen and extraordinary calamity could have occurred between
the time when he parted from his father, and the moment when,
drawn back by his screams, he rushed into the glade? It was
something terrible and deadly. What could it be? Might not the
nature of the injuries reveal something to my medical instincts?
I rang the bell and called for the weekly county paper, which
contained a verbatim account of the inquest. In the surgeon's
deposition it was stated that the posterior third of the left
parietal bone and the left half of the occipital bone had been
shattered by a heavy blow from a blunt weapon. I marked the spot
upon my own head. Clearly such a blow must have been struck from
behind. That was to some extent in favour of the accused, as when
seen quarrelling he was face to face with his father. Still, it
did not go for very much, for the older man might have turned his
back before the blow fell. Still, it might be worth while to call
Holmes' attention to it. Then there was the peculiar dying
reference to a rat. What could that mean? It could not be
delirium. A man dying from a sudden blow does not commonly become
delirious. No, it was more likely to be an attempt to explain how
he met his fate. But what could it indicate? I cudgelled my
brains to find some possible explanation. And then the incident
of the grey cloth seen by young McCarthy. If that were true the
murderer must have dropped some part of his dress, presumably his
overcoat, in his flight, and must have had the hardihood to
return and to carry it away at the instant when the son was
kneeling with his back turned not a dozen paces off. What a
tissue of mysteries and improbabilities the whole thing was! I
did not wonder at Lestrade's opinion, and yet I had so much faith
in Sherlock Holmes' insight that I could not lose hope as long
as every fresh fact seemed to strengthen his conviction of young
McCarthy's innocence.
It was late before Sherlock Holmes returned. He came back alone,
for Lestrade was staying in lodgings in the town.
"The glass still keeps very high," he remarked as he sat down.
"It is of importance that it should not rain before we are able
to go over the ground. On the other hand, a man should be at his
very best and keenest for such nice work as that, and I did not
wish to do it when fagged by a long journey. I have seen young
McCarthy."
"And what did you learn from him?"
"Nothing."
"Could he throw no light?"
"None at all. I was inclined to think at one time that he knew
who had done it and was screening him or her, but I am convinced
now that he is as puzzled as everyone else. He is not a very
quick-witted youth, though comely to look at and, I should think,
sound at heart."
"I cannot admire his taste," I remarked, "if it is indeed a fact
that he was averse to a marriage with so charming a young lady as
this Miss Turner."
"Ah, thereby hangs a rather painful tale. This fellow is madly,
insanely, in love with her, but some two years ago, when he was
only a lad, and before he really knew her, for she had been away
five years at a boarding-school, what does the idiot do but get
into the clutches of a barmaid in Bristol and marry her at a
registry office? No one knows a word of the matter, but you can
imagine how maddening it must be to him to be upbraided for not
doing what he would give his very eyes to do, but what he knows
to be absolutely impossible. It was sheer frenzy of this sort
which made him throw his hands up into the air when his father,
at their last interview, was goading him on to propose to Miss
Turner. On the other hand, he had no means of supporting himself,
and his father, who was by all accounts a very hard man, would
have thrown him over utterly had he known the truth. It was with
his barmaid wife that he had spent the last three days in
Bristol, and his father did not know where he was. Mark that
point. It is of importance. Good has come out of evil, however,
for the barmaid, finding from the papers that he is in serious
trouble and likely to be hanged, has thrown him over utterly and
has written to him to say that she has a husband already in the
Bermuda Dockyard, so that there is really no tie between them. I
think that that bit of news has consoled young McCarthy for all
that he has suffered."
"But if he is innocent, who has done it?"
"Ah! who? I would call your attention very particularly to two
points. One is that the murdered man had an appointment with
someone at the pool, and that the someone could not have been his
son, for his son was away, and he did not know when he would
return. The second is that the murdered man was heard to cry
'Cooee!' before he knew that his son had returned. Those are the
crucial points upon which the case depends. And now let us talk
about George Meredith, if you please, and we shall leave all
minor matters until to-morrow."
There was no rain, as Holmes had foretold, and the morning broke
bright and cloudless. At nine o'clock Lestrade called for us with
the carriage, and we set off for Hatherley Farm and the Boscombe
Pool.
"There is serious news this morning," Lestrade observed. "It is
said that Mr. Turner, of the Hall, is so ill that his life is
despaired of."
"An elderly man, I presume?" said Holmes.
"About sixty; but his constitution has been shattered by his life
abroad, and he has been in failing health for some time. This
business has had a very bad effect upon him. He was an old friend
of McCarthy's, and, I may add, a great benefactor to him, for I
have learned that he gave him Hatherley Farm rent free."
"Indeed! That is interesting," said Holmes.
"Oh, yes! In a hundred other ways he has helped him. Everybody
about here speaks of his kindness to him."
"Really! Does it not strike you as a little singular that this
McCarthy, who appears to have had little of his own, and to have
been under such obligations to Turner, should still talk of
marrying his son to Turner's daughter, who is, presumably,
heiress to the estate, and that in such a very cocksure manner,
as if it were merely a case of a proposal and all else would
follow? It is the more strange, since we know that Turner himself
was averse to the idea. The daughter told us as much. Do you not
deduce something from that?"
"We have got to the deductions and the inferences," said
Lestrade, winking at me. "I find it hard enough to tackle facts,
Holmes, without flying away after theories and fancies."
"You are right," said Holmes demurely; "you do find it very hard
to tackle the facts."
"Anyhow, I have grasped one fact which you seem to find it
difficult to get hold of," replied Lestrade with some warmth.
"And that is--"
"That McCarthy senior met his death from McCarthy junior and that
all theories to the contrary are the merest moonshine."
"Well, moonshine is a brighter thing than fog," said Holmes,
laughing. "But I am very much mistaken if this is not Hatherley
Farm upon the left."
"Yes, that is it." It was a widespread, comfortable-looking
building, two-storied, slate-roofed, with great yellow blotches
of lichen upon the grey walls. The drawn blinds and the smokeless
chimneys, however, gave it a stricken look, as though the weight
of this horror still lay heavy upon it. We called at the door,
when the maid, at Holmes' request, showed us the boots which her
master wore at the time of his death, and also a pair of the
son's, though not the pair which he had then had. Having measured
these very carefully from seven or eight different points, Holmes
desired to be led to the court-yard, from which we all followed
the winding track which led to Boscombe Pool.
Sherlock Holmes was transformed when he was hot upon such a scent
as this. Men who had only known the quiet thinker and logician of
Baker Street would have failed to recognise him. His face flushed
and darkened. His brows were drawn into two hard black lines,
while his eyes shone out from beneath them with a steely glitter.
His face was bent downward, his shoulders bowed, his lips
compressed, and the veins stood out like whipcord in his long,
sinewy neck. His nostrils seemed to dilate with a purely animal
lust for the chase, and his mind was so absolutely concentrated
upon the matter before him that a question or remark fell
unheeded upon his ears, or, at the most, only provoked a quick,
impatient snarl in reply. Swiftly and silently he made his way
along the track which ran through the meadows, and so by way of
the woods to the Boscombe Pool. It was damp, marshy ground, as is
all that district, and there were marks of many feet, both upon
the path and amid the short grass which bounded it on either
side. Sometimes Holmes would hurry on, sometimes stop dead, and
once he made quite a little detour into the meadow. Lestrade and
I walked behind him, the detective indifferent and contemptuous,
while I watched my friend with the interest which sprang from the
conviction that every one of his actions was directed towards a
definite end.
The Boscombe Pool, which is a little reed-girt sheet of water
some fifty yards across, is situated at the boundary between the
Hatherley Farm and the private park of the wealthy Mr. Turner.
Above the woods which lined it upon the farther side we could see
the red, jutting pinnacles which marked the site of the rich
landowner's dwelling. On the Hatherley side of the pool the woods
grew very thick, and there was a narrow belt of sodden grass
twenty paces across between the edge of the trees and the reeds
which lined the lake. Lestrade showed us the exact spot at which
the body had been found, and, indeed, so moist was the ground,
that I could plainly see the traces which had been left by the
fall of the stricken man. To Holmes, as I could see by his eager
face and peering eyes, very many other things were to be read
upon the trampled grass. He ran round, like a dog who is picking
up a scent, and then turned upon my companion.
"What did you go into the pool for?" he asked.
"I fished about with a rake. I thought there might be some weapon
or other trace. But how on earth--"
"Oh, tut, tut! I have no time! That left foot of yours with its
inward twist is all over the place. A mole could trace it, and
there it vanishes among the reeds. Oh, how simple it would all
have been had I been here before they came like a herd of buffalo
and wallowed all over it. Here is where the party with the
lodge-keeper came, and they have covered all tracks for six or
eight feet round the body. But here are three separate tracks of
the same feet." He drew out a lens and lay down upon his
waterproof to have a better view, talking all the time rather to
himself than to us. "These are young McCarthy's feet. Twice he
was walking, and once he ran swiftly, so that the soles are
deeply marked and the heels hardly visible. That bears out his
story. He ran when he saw his father on the ground. Then here are
the father's feet as he paced up and down. What is this, then? It
is the butt-end of the gun as the son stood listening. And this?
Ha, ha! What have we here? Tiptoes! tiptoes! Square, too, quite
unusual boots! They come, they go, they come again--of course
that was for the cloak. Now where did they come from?" He ran up
and down, sometimes losing, sometimes finding the track until we
were well within the edge of the wood and under the shadow of a
great beech, the largest tree in the neighbourhood. Holmes traced
his way to the farther side of this and lay down once more upon
his face with a little cry of satisfaction. For a long time he
remained there, turning over the leaves and dried sticks,
gathering up what seemed to me to be dust into an envelope and
examining with his lens not only the ground but even the bark of
the tree as far as he could reach. A jagged stone was lying among
the moss, and this also he carefully examined and retained. Then
he followed a pathway through the wood until he came to the
highroad, where all traces were lost.
"It has been a case of considerable interest," he remarked,
returning to his natural manner. "I fancy that this grey house on
the right must be the lodge. I think that I will go in and have a
word with Moran, and perhaps write a little note. Having done
that, we may drive back to our luncheon. You may walk to the cab,
and I shall be with you presently."
It was about ten minutes before we regained our cab and drove
back into Ross, Holmes still carrying with him the stone which he
had picked up in the wood.
"This may interest you, Lestrade," he remarked, holding it out.
"The murder was done with it."
"I see no marks."
"There are none."
"How do you know, then?"
"The grass was growing under it. It had only lain there a few
days. There was no sign of a place whence it had been taken. It
corresponds with the injuries. There is no sign of any other
weapon."
"And the murderer?"
"Is a tall man, left-handed, limps with the right leg, wears
thick-soled shooting-boots and a grey cloak, smokes Indian
cigars, uses a cigar-holder, and carries a blunt pen-knife in his
pocket. There are several other indications, but these may be
enough to aid us in our search."
Lestrade laughed. "I am afraid that I am still a sceptic," he
said. "Theories are all very well, but we have to deal with a
hard-headed British jury."
"Nous verrons," answered Holmes calmly. "You work your own
method, and I shall work mine. I shall be busy this afternoon,
and shall probably return to London by the evening train."
"And leave your case unfinished?"
"No, finished."
"But the mystery?"
"It is solved."
"Who was the criminal, then?"
"The gentleman I describe."
"But who is he?"
"Surely it would not be difficult to find out. This is not such a
populous neighbourhood."
Lestrade shrugged his shoulders. "I am a practical man," he said,
"and I really cannot undertake to go about the country looking
for a left-handed gentleman with a game leg. I should become the
laughing-stock of Scotland Yard."
"All right," said Holmes quietly. "I have given you the chance.
Here are your lodgings. Good-bye. I shall drop you a line before
I leave."
Having left Lestrade at his rooms, we drove to our hotel, where
we found lunch upon the table. Holmes was silent and buried in
thought with a pained expression upon his face, as one who finds
himself in a perplexing position.
"Look here, Watson," he said when the cloth was cleared "just sit
down in this chair and let me preach to you for a little. I don't
know quite what to do, and I should value your advice. Light a
cigar and let me expound."
"Pray do so."
"Well, now, in considering this case there are two points about
young McCarthy's narrative which struck us both instantly,
although they impressed me in his favour and you against him. One
was the fact that his father should, according to his account,
cry 'Cooee!' before seeing him. The other was his singular dying
reference to a rat. He mumbled several words, you understand, but
that was all that caught the son's ear. Now from this double
point our research must commence, and we will begin it by
presuming that what the lad says is absolutely true."
"What of this 'Cooee!' then?"
"Well, obviously it could not have been meant for the son. The
son, as far as he knew, was in Bristol. It was mere chance that
he was within earshot. The 'Cooee!' was meant to attract the
attention of whoever it was that he had the appointment with. But
'Cooee' is a distinctly Australian cry, and one which is used
between Australians. There is a strong presumption that the
person whom McCarthy expected to meet him at Boscombe Pool was
someone who had been in Australia."
"What of the rat, then?"
Sherlock Holmes took a folded paper from his pocket and flattened
it out on the table. "This is a map of the Colony of Victoria,"
he said. "I wired to Bristol for it last night." He put his hand
over part of the map. "What do you read?"
"ARAT," I read.
"And now?" He raised his hand.
"BALLARAT."
"Quite so. That was the word the man uttered, and of which his
son only caught the last two syllables. He was trying to utter
the name of his murderer. So and so, of Ballarat."
"It is wonderful!" I exclaimed.
"It is obvious. And now, you see, I had narrowed the field down
considerably. The possession of a grey garment was a third point
which, granting the son's statement to be correct, was a
certainty. We have come now out of mere vagueness to the definite
conception of an Australian from Ballarat with a grey cloak."
"Certainly."
"And one who was at home in the district, for the pool can only
be approached by the farm or by the estate, where strangers could
hardly wander."
"Quite so."
"Then comes our expedition of to-day. By an examination of the
ground I gained the trifling details which I gave to that
imbecile Lestrade, as to the personality of the criminal."
"But how did you gain them?"
"You know my method. It is founded upon the observation of
trifles."
"His height I know that you might roughly judge from the length
of his stride. His boots, too, might be told from their traces."
"Yes, they were peculiar boots."
"But his lameness?"
"The impression of his right foot was always less distinct than
his left. He put less weight upon it. Why? Because he limped--he
was lame."
"But his left-handedness."
"You were yourself struck by the nature of the injury as recorded
by the surgeon at the inquest. The blow was struck from
immediately behind, and yet was upon the left side. Now, how can
that be unless it were by a left-handed man? He had stood behind
that tree during the interview between the father and son. He had
even smoked there. I found the ash of a cigar, which my special
knowledge of tobacco ashes enables me to pronounce as an Indian
cigar. I have, as you know, devoted some attention to this, and
written a little monograph on the ashes of 140 different
varieties of pipe, cigar, and cigarette tobacco. Having found the
ash, I then looked round and discovered the stump among the moss
where he had tossed it. It was an Indian cigar, of the variety
which are rolled in Rotterdam."
"And the cigar-holder?"
"I could see that the end had not been in his mouth. Therefore he
used a holder. The tip had been cut off, not bitten off, but the
cut was not a clean one, so I deduced a blunt pen-knife."
"Holmes," I said, "you have drawn a net round this man from which
he cannot escape, and you have saved an innocent human life as
truly as if you had cut the cord which was hanging him. I see the
direction in which all this points. The culprit is--"
"Mr. John Turner," cried the hotel waiter, opening the door of
our sitting-room, and ushering in a visitor.
The man who entered was a strange and impressive figure. His
slow, limping step and bowed shoulders gave the appearance of
decrepitude, and yet his hard, deep-lined, craggy features, and
his enormous limbs showed that he was possessed of unusual
strength of body and of character. His tangled beard, grizzled
hair, and outstanding, drooping eyebrows combined to give an air
of dignity and power to his appearance, but his face was of an
ashen white, while his lips and the corners of his nostrils were
tinged with a shade of blue. It was clear to me at a glance that
he was in the grip of some deadly and chronic disease.
"Pray sit down on the sofa," said Holmes gently. "You had my
note?"
"Yes, the lodge-keeper brought it up. You said that you wished to
see me here to avoid scandal."
"I thought people would talk if I went to the Hall."
"And why did you wish to see me?" He looked across at my
companion with despair in his weary eyes, as though his question
was already answered.
"Yes," said Holmes, answering the look rather than the words. "It
is so. I know all about McCarthy."
The old man sank his face in his hands. "God help me!" he cried.
"But I would not have let the young man come to harm. I give you
my word that I would have spoken out if it went against him at
the Assizes."
"I am glad to hear you say so," said Holmes gravely.
"I would have spoken now had it not been for my dear girl. It
would break her heart--it will break her heart when she hears
that I am arrested."
"It may not come to that," said Holmes.
"What?"
"I am no official agent. I understand that it was your daughter
who required my presence here, and I am acting in her interests.
Young McCarthy must be got off, however."
"I am a dying man," said old Turner. "I have had diabetes for
years. My doctor says it is a question whether I shall live a
month. Yet I would rather die under my own roof than in a gaol."
Holmes rose and sat down at the table with his pen in his hand
and a bundle of paper before him. "Just tell us the truth," he
said. "I shall jot down the facts. You will sign it, and Watson
here can witness it. Then I could produce your confession at the
last extremity to save young McCarthy. I promise you that I shall
not use it unless it is absolutely needed."
"It's as well," said the old man; "it's a question whether I
shall live to the Assizes, so it matters little to me, but I
should wish to spare Alice the shock. And now I will make the
thing clear to you; it has been a long time in the acting, but
will not take me long to tell.
"You didn't know this dead man, McCarthy. He was a devil
incarnate. I tell you that. God keep you out of the clutches of
such a man as he. His grip has been upon me these twenty years,
and he has blasted my life. I'll tell you first how I came to be
in his power.
"It was in the early '60's at the diggings. I was a young chap
then, hot-blooded and reckless, ready to turn my hand at
anything; I got among bad companions, took to drink, had no luck
with my claim, took to the bush, and in a word became what you
would call over here a highway robber. There were six of us, and
we had a wild, free life of it, sticking up a station from time
to time, or stopping the wagons on the road to the diggings.
Black Jack of Ballarat was the name I went under, and our party
is still remembered in the colony as the Ballarat Gang.
"One day a gold convoy came down from Ballarat to Melbourne, and
we lay in wait for it and attacked it. There were six troopers
and six of us, so it was a close thing, but we emptied four of
their saddles at the first volley. Three of our boys were killed,
however, before we got the swag. I put my pistol to the head of
the wagon-driver, who was this very man McCarthy. I wish to the
Lord that I had shot him then, but I spared him, though I saw his
wicked little eyes fixed on my face, as though to remember every
feature. We got away with the gold, became wealthy men, and made
our way over to England without being suspected. There I parted
from my old pals and determined to settle down to a quiet and
respectable life. I bought this estate, which chanced to be in
the market, and I set myself to do a little good with my money,
to make up for the way in which I had earned it. I married, too,
and though my wife died young she left me my dear little Alice.
Even when she was just a baby her wee hand seemed to lead me down
the right path as nothing else had ever done. In a word, I turned
over a new leaf and did my best to make up for the past. All was
going well when McCarthy laid his grip upon me.
"I had gone up to town about an investment, and I met him in
Regent Street with hardly a coat to his back or a boot to his
foot.
"'Here we are, Jack,' says he, touching me on the arm; 'we'll be
as good as a family to you. There's two of us, me and my son, and
you can have the keeping of us. If you don't--it's a fine,
law-abiding country is England, and there's always a policeman
within hail.'
"Well, down they came to the west country, there was no shaking
them off, and there they have lived rent free on my best land
ever since. There was no rest for me, no peace, no forgetfulness;
turn where I would, there was his cunning, grinning face at my
elbow. It grew worse as Alice grew up, for he soon saw I was more
afraid of her knowing my past than of the police. Whatever he
wanted he must have, and whatever it was I gave him without
question, land, money, houses, until at last he asked a thing
which I could not give. He asked for Alice.
"His son, you see, had grown up, and so had my girl, and as I was
known to be in weak health, it seemed a fine stroke to him that
his lad should step into the whole property. But there I was
firm. I would not have his cursed stock mixed with mine; not that
I had any dislike to the lad, but his blood was in him, and that
was enough. I stood firm. McCarthy threatened. I braved him to do
his worst. We were to meet at the pool midway between our houses
to talk it over.
"When I went down there I found him talking with his son, so I
smoked a cigar and waited behind a tree until he should be alone.
But as I listened to his talk all that was black and bitter in
me seemed to come uppermost. He was urging his son to marry my
daughter with as little regard for what she might think as if she
were a slut from off the streets. It drove me mad to think that I
and all that I held most dear should be in the power of such a
man as this. Could I not snap the bond? I was already a dying and
a desperate man. Though clear of mind and fairly strong of limb,
I knew that my own fate was sealed. But my memory and my girl!
Both could be saved if I could but silence that foul tongue. I
did it, Mr. Holmes. I would do it again. Deeply as I have sinned,
I have led a life of martyrdom to atone for it. But that my girl
should be entangled in the same meshes which held me was more
than I could suffer. I struck him down with no more compunction
than if he had been some foul and venomous beast. His cry brought
back his son; but I had gained the cover of the wood, though I
was forced to go back to fetch the cloak which I had dropped in
my flight. That is the true story, gentlemen, of all that
occurred."
"Well, it is not for me to judge you," said Holmes as the old man
signed the statement which had been drawn out. "I pray that we
may never be exposed to such a temptation."
"I pray not, sir. And what do you intend to do?"
"In view of your health, nothing. You are yourself aware that you
will soon have to answer for your deed at a higher court than the
Assizes. I will keep your confession, and if McCarthy is
condemned I shall be forced to use it. If not, it shall never be
seen by mortal eye; and your secret, whether you be alive or
dead, shall be safe with us."
"Farewell, then," said the old man solemnly. "Your own deathbeds,
when they come, will be the easier for the thought of the peace
which you have given to mine." Tottering and shaking in all his
giant frame, he stumbled slowly from the room.
"God help us!" said Holmes after a long silence. "Why does fate
play such tricks with poor, helpless worms? I never hear of such
a case as this that I do not think of Baxter's words, and say,
'There, but for the grace of God, goes Sherlock Holmes.'"
James McCarthy was acquitted at the Assizes on the strength of a
number of objections which had been drawn out by Holmes and
submitted to the defending counsel. Old Turner lived for seven
months after our interview, but he is now dead; and there is
every prospect that the son and daughter may come to live happily
together in ignorance of the black cloud which rests upon their
past.
ADVENTURE V. THE FIVE ORANGE PIPS
When I glance over my notes and records of the Sherlock Holmes
cases between the years '82 and '90, I am faced by so many which
present strange and interesting features that it is no easy
matter to know which to choose and which to leave. Some, however,
have already gained publicity through the papers, and others have
not offered a field for those peculiar qualities which my friend
possessed in so high a degree, and which it is the object of
these papers to illustrate. Some, too, have baffled his
analytical skill, and would be, as narratives, beginnings without
an ending, while others have been but partially cleared up, and
have their explanations founded rather upon conjecture and
surmise than on that absolute logical proof which was so dear to
him. There is, however, one of these last which was so remarkable
in its details and so startling in its results that I am tempted
to give some account of it in spite of the fact that there are
points in connection with it which never have been, and probably
never will be, entirely cleared up.
The year '87 furnished us with a long series of cases of greater
or less interest, of which I retain the records. Among my
headings under this one twelve months I find an account of the
adventure of the Paradol Chamber, of the Amateur Mendicant
Society, who held a luxurious club in the lower vault of a
furniture warehouse, of the facts connected with the loss of the
British barque "Sophy Anderson", of the singular adventures of the
Grice Patersons in the island of Uffa, and finally of the
Camberwell poisoning case. In the latter, as may be remembered,
Sherlock Holmes was able, by winding up the dead man's watch, to
prove that it had been wound up two hours before, and that
therefore the deceased had gone to bed within that time--a
deduction which was of the greatest importance in clearing up the
case. All these I may sketch out at some future date, but none of
them present such singular features as the strange train of
circumstances which I have now taken up my pen to describe.
It was in the latter days of September, and the equinoctial gales
had set in with exceptional violence. All day the wind had
screamed and the rain had beaten against the windows, so that
even here in the heart of great, hand-made London we were forced
to raise our minds for the instant from the routine of life and
to recognise the presence of those great elemental forces which
shriek at mankind through the bars of his civilisation, like
untamed beasts in a cage. As evening drew in, the storm grew
higher and louder, and the wind cried and sobbed like a child in
the chimney. Sherlock Holmes sat moodily at one side of the
fireplace cross-indexing his records of crime, while I at the
other was deep in one of Clark Russell's fine sea-stories until
the howl of the gale from without seemed to blend with the text,
and the splash of the rain to lengthen out into the long swash of
the sea waves. My wife was on a visit to her mother's, and for a
few days I was a dweller once more in my old quarters at Baker
Street.
"Why," said I, glancing up at my companion, "that was surely the
bell. Who could come to-night? Some friend of yours, perhaps?"
"Except yourself I have none," he answered. "I do not encourage
visitors."
"A client, then?"
"If so, it is a serious case. Nothing less would bring a man out
on such a day and at such an hour. But I take it that it is more
likely to be some crony of the landlady's."
Sherlock Holmes was wrong in his conjecture, however, for there
came a step in the passage and a tapping at the door. He
stretched out his long arm to turn the lamp away from himself and
towards the vacant chair upon which a newcomer must sit.
"Come in!" said he.
The man who entered was young, some two-and-twenty at the
outside, well-groomed and trimly clad, with something of
refinement and delicacy in his bearing. The streaming umbrella
which he held in his hand, and his long shining waterproof told
of the fierce weather through which he had come. He looked about
him anxiously in the glare of the lamp, and I could see that his
face was pale and his eyes heavy, like those of a man who is
weighed down with some great anxiety.
"I owe you an apology," he said, raising his golden pince-nez to
his eyes. "I trust that I am not intruding. I fear that I have
brought some traces of the storm and rain into your snug
chamber."
"Give me your coat and umbrella," said Holmes. "They may rest
here on the hook and will be dry presently. You have come up from
the south-west, I see."
"Yes, from Horsham."
"That clay and chalk mixture which I see upon your toe caps is
quite distinctive."
"I have come for advice."
"That is easily got."
"And help."
"That is not always so easy."
"I have heard of you, Mr. Holmes. I heard from Major Prendergast
how you saved him in the Tankerville Club scandal."
"Ah, of course. He was wrongfully accused of cheating at cards."
"He said that you could solve anything."
"He said too much."
"That you are never beaten."
"I have been beaten four times--three times by men, and once by a
woman."
"But what is that compared with the number of your successes?"
"It is true that I have been generally successful."
"Then you may be so with me."
"I beg that you will draw your chair up to the fire and favour me
with some details as to your case."
"It is no ordinary one."
"None of those which come to me are. I am the last court of
appeal."
"And yet I question, sir, whether, in all your experience, you
have ever listened to a more mysterious and inexplicable chain of
events than those which have happened in my own family."
"You fill me with interest," said Holmes. "Pray give us the
essential facts from the commencement, and I can afterwards
question you as to those details which seem to me to be most
important."
The young man pulled his chair up and pushed his wet feet out
towards the blaze.
"My name," said he, "is John Openshaw, but my own affairs have,
as far as I can understand, little to do with this awful
business. It is a hereditary matter; so in order to give you an
idea of the facts, I must go back to the commencement of the
affair.
"You must know that my grandfather had two sons--my uncle Elias
and my father Joseph. My father had a small factory at Coventry,
which he enlarged at the time of the invention of bicycling. He
was a patentee of the Openshaw unbreakable tire, and his business
met with such success that he was able to sell it and to retire
upon a handsome competence.
"My uncle Elias emigrated to America when he was a young man and
became a planter in Florida, where he was reported to have done
very well. At the time of the war he fought in Jackson's army,
and afterwards under Hood, where he rose to be a colonel. When
Lee laid down his arms my uncle returned to his plantation, where
he remained for three or four years. About 1869 or 1870 he came
back to Europe and took a small estate in Sussex, near Horsham.
He had made a very considerable fortune in the States, and his
reason for leaving them was his aversion to the negroes, and his
dislike of the Republican policy in extending the franchise to
them. He was a singular man, fierce and quick-tempered, very
foul-mouthed when he was angry, and of a most retiring
disposition. During all the years that he lived at Horsham, I
doubt if ever he set foot in the town. He had a garden and two or
three fields round his house, and there he would take his
exercise, though very often for weeks on end he would never leave
his room. He drank a great deal of brandy and smoked very
heavily, but he would see no society and did not want any
friends, not even his own brother.
"He didn't mind me; in fact, he took a fancy to me, for at the
time when he saw me first I was a youngster of twelve or so. This
would be in the year 1878, after he had been eight or nine years
in England. He begged my father to let me live with him and he
was very kind to me in his way. When he was sober he used to be
fond of playing backgammon and draughts with me, and he would
make me his representative both with the servants and with the
tradespeople, so that by the time that I was sixteen I was quite
master of the house. I kept all the keys and could go where I
liked and do what I liked, so long as I did not disturb him in
his privacy. There was one singular exception, however, for he
had a single room, a lumber-room up among the attics, which was
invariably locked, and which he would never permit either me or
anyone else to enter. With a boy's curiosity I have peeped
through the keyhole, but I was never able to see more than such a
collection of old trunks and bundles as would be expected in such
a room.
"One day--it was in March, 1883--a letter with a foreign stamp
lay upon the table in front of the colonel's plate. It was not a
common thing for him to receive letters, for his bills were all
paid in ready money, and he had no friends of any sort. 'From
India!' said he as he took it up, 'Pondicherry postmark! What can
this be?' Opening it hurriedly, out there jumped five little
dried orange pips, which pattered down upon his plate. I began to
laugh at this, but the laugh was struck from my lips at the sight
of his face. His lip had fallen, his eyes were protruding, his
skin the colour of putty, and he glared at the envelope which he
still held in his trembling hand, 'K. K. K.!' he shrieked, and
then, 'My God, my God, my sins have overtaken me!'
"'What is it, uncle?' I cried.
"'Death,' said he, and rising from the table he retired to his
room, leaving me palpitating with horror. I took up the envelope
and saw scrawled in red ink upon the inner flap, just above the
gum, the letter K three times repeated. There was nothing else
save the five dried pips. What could be the reason of his
overpowering terror? I left the breakfast-table, and as I
ascended the stair I met him coming down with an old rusty key,
which must have belonged to the attic, in one hand, and a small
brass box, like a cashbox, in the other.
"'They may do what they like, but I'll checkmate them still,'
said he with an oath. 'Tell Mary that I shall want a fire in my
room to-day, and send down to Fordham, the Horsham lawyer.'
"I did as he ordered, and when the lawyer arrived I was asked to
step up to the room. The fire was burning brightly, and in the
grate there was a mass of black, fluffy ashes, as of burned
paper, while the brass box stood open and empty beside it. As I
glanced at the box I noticed, with a start, that upon the lid was
printed the treble K which I had read in the morning upon the
envelope.
"'I wish you, John,' said my uncle, 'to witness my will. I leave
my estate, with all its advantages and all its disadvantages, to
my brother, your father, whence it will, no doubt, descend to
you. If you can enjoy it in peace, well and good! If you find you
cannot, take my advice, my boy, and leave it to your deadliest
enemy. I am sorry to give you such a two-edged thing, but I can't
say what turn things are going to take. Kindly sign the paper
where Mr. Fordham shows you.'
"I signed the paper as directed, and the lawyer took it away with
him. The singular incident made, as you may think, the deepest
impression upon me, and I pondered over it and turned it every
way in my mind without being able to make anything of it. Yet I
could not shake off the vague feeling of dread which it left
behind, though the sensation grew less keen as the weeks passed
and nothing happened to disturb the usual routine of our lives. I
could see a change in my uncle, however. He drank more than ever,
and he was less inclined for any sort of society. Most of his
time he would spend in his room, with the door locked upon the
inside, but sometimes he would emerge in a sort of drunken frenzy
and would burst out of the house and tear about the garden with a
revolver in his hand, screaming out that he was afraid of no man,
and that he was not to be cooped up, like a sheep in a pen, by
man or devil. When these hot fits were over, however, he would
rush tumultuously in at the door and lock and bar it behind him,
like a man who can brazen it out no longer against the terror
which lies at the roots of his soul. At such times I have seen
his face, even on a cold day, glisten with moisture, as though it
were new raised from a basin.
"Well, to come to an end of the matter, Mr. Holmes, and not to
abuse your patience, there came a night when he made one of those
drunken sallies from which he never came back. We found him, when
we went to search for him, face downward in a little
green-scummed pool, which lay at the foot of the garden. There
was no sign of any violence, and the water was but two feet deep,
so that the jury, having regard to his known eccentricity,
brought in a verdict of 'suicide.' But I, who knew how he winced
from the very thought of death, had much ado to persuade myself
that he had gone out of his way to meet it. The matter passed,
however, and my father entered into possession of the estate, and
of some 14,000 pounds, which lay to his credit at the bank."
"One moment," Holmes interposed, "your statement is, I foresee,
one of the most remarkable to which I have ever listened. Let me
have the date of the reception by your uncle of the letter, and
the date of his supposed suicide."
"The letter arrived on March 10, 1883. His death was seven weeks
later, upon the night of May 2nd."
"Thank you. Pray proceed."
"When my father took over the Horsham property, he, at my
request, made a careful examination of the attic, which had been
always locked up. We found the brass box there, although its
contents had been destroyed. On the inside of the cover was a
paper label, with the initials of K. K. K. repeated upon it, and
'Letters, memoranda, receipts, and a register' written beneath.
These, we presume, indicated the nature of the papers which had
been destroyed by Colonel Openshaw. For the rest, there was
nothing of much importance in the attic save a great many
scattered papers and note-books bearing upon my uncle's life in
America. Some of them were of the war time and showed that he had
done his duty well and had borne the repute of a brave soldier.
Others were of a date during the reconstruction of the Southern
states, and were mostly concerned with politics, for he had
evidently taken a strong part in opposing the carpet-bag
politicians who had been sent down from the North.
"Well, it was the beginning of '84 when my father came to live at
Horsham, and all went as well as possible with us until the
January of '85. On the fourth day after the new year I heard my
father give a sharp cry of surprise as we sat together at the
breakfast-table. There he was, sitting with a newly opened
envelope in one hand and five dried orange pips in the
outstretched palm of the other one. He had always laughed at what
he called my cock-and-bull story about the colonel, but he looked
very scared and puzzled now that the same thing had come upon
himself.
"'Why, what on earth does this mean, John?' he stammered.
"My heart had turned to lead. 'It is K. K. K.,' said I.
"He looked inside the envelope. 'So it is,' he cried. 'Here are
the very letters. But what is this written above them?'
"'Put the papers on the sundial,' I read, peeping over his
shoulder.
"'What papers? What sundial?' he asked.
"'The sundial in the garden. There is no other,' said I; 'but the
papers must be those that are destroyed.'
"'Pooh!' said he, gripping hard at his courage. 'We are in a
civilised land here, and we can't have tomfoolery of this kind.
Where does the thing come from?'
"'From Dundee,' I answered, glancing at the postmark.
"'Some preposterous practical joke,' said he. 'What have I to do
with sundials and papers? I shall take no notice of such
nonsense.'
"'I should certainly speak to the police,' I said.
"'And be laughed at for my pains. Nothing of the sort.'
"'Then let me do so?'
"'No, I forbid you. I won't have a fuss made about such
nonsense.'
"It was in vain to argue with him, for he was a very obstinate
man. I went about, however, with a heart which was full of
forebodings.
"On the third day after the coming of the letter my father went
from home to visit an old friend of his, Major Freebody, who is
in command of one of the forts upon Portsdown Hill. I was glad
that he should go, for it seemed to me that he was farther from
danger when he was away from home. In that, however, I was in
error. Upon the second day of his absence I received a telegram
from the major, imploring me to come at once. My father had
fallen over one of the deep chalk-pits which abound in the
neighbourhood, and was lying senseless, with a shattered skull. I
hurried to him, but he passed away without having ever recovered
his consciousness. He had, as it appears, been returning from
Fareham in the twilight, and as the country was unknown to him,
and the chalk-pit unfenced, the jury had no hesitation in
bringing in a verdict of 'death from accidental causes.'
Carefully as I examined every fact connected with his death, I
was unable to find anything which could suggest the idea of
murder. There were no signs of violence, no footmarks, no
robbery, no record of strangers having been seen upon the roads.
And yet I need not tell you that my mind was far from at ease,
and that I was well-nigh certain that some foul plot had been
woven round him.
"In this sinister way I came into my inheritance. You will ask me
why I did not dispose of it? I answer, because I was well
convinced that our troubles were in some way dependent upon an
incident in my uncle's life, and that the danger would be as
pressing in one house as in another.
"It was in January, '85, that my poor father met his end, and two
years and eight months have elapsed since then. During that time
I have lived happily at Horsham, and I had begun to hope that
this curse had passed away from the family, and that it had ended
with the last generation. I had begun to take comfort too soon,
however; yesterday morning the blow fell in the very shape in
which it had come upon my father."
The young man took from his waistcoat a crumpled envelope, and
turning to the table he shook out upon it five little dried
orange pips.
"This is the envelope," he continued. "The postmark is
London--eastern division. Within are the very words which were
upon my father's last message: 'K. K. K.'; and then 'Put the
papers on the sundial.'"
"What have you done?" asked Holmes.
"Nothing."
"Nothing?"
"To tell the truth"--he sank his face into his thin, white
hands--"I have felt helpless. I have felt like one of those poor
rabbits when the snake is writhing towards it. I seem to be in
the grasp of some resistless, inexorable evil, which no foresight
and no precautions can guard against."
"Tut! tut!" cried Sherlock Holmes. "You must act, man, or you are
lost. Nothing but energy can save you. This is no time for
despair."
"I have seen the police."
"Ah!"
"But they listened to my story with a smile. I am convinced that
the inspector has formed the opinion that the letters are all
practical jokes, and that the deaths of my relations were really
accidents, as the jury stated, and were not to be connected with
the warnings."
Holmes shook his clenched hands in the air. "Incredible
imbecility!" he cried.
"They have, however, allowed me a policeman, who may remain in
the house with me."
"Has he come with you to-night?"
"No. His orders were to stay in the house."
Again Holmes raved in the air.
"Why did you come to me," he cried, "and, above all, why did you
not come at once?"
"I did not know. It was only to-day that I spoke to Major
Prendergast about my troubles and was advised by him to come to
you."
"It is really two days since you had the letter. We should have
acted before this. You have no further evidence, I suppose, than
that which you have placed before us--no suggestive detail which
might help us?"
"There is one thing," said John Openshaw. He rummaged in his coat
pocket, and, drawing out a piece of discoloured, blue-tinted
paper, he laid it out upon the table. "I have some remembrance,"
said he, "that on the day when my uncle burned the papers I
observed that the small, unburned margins which lay amid the
ashes were of this particular colour. I found this single sheet
upon the floor of his room, and I am inclined to think that it
may be one of the papers which has, perhaps, fluttered out from
among the others, and in that way has escaped destruction. Beyond
the mention of pips, I do not see that it helps us much. I think
myself that it is a page from some private diary. The writing is
undoubtedly my uncle's."
Holmes moved the lamp, and we both bent over the sheet of paper,
which showed by its ragged edge that it had indeed been torn from
a book. It was headed, "March, 1869," and beneath were the
following enigmatical notices:
"4th. Hudson came. Same old platform.
"7th. Set the pips on McCauley, Paramore, and
John Swain, of St. Augustine.
"9th. McCauley cleared.
"10th. John Swain cleared.
"12th. Visited Paramore. All well."
"Thank you!" said Holmes, folding up the paper and returning it
to our visitor. "And now you must on no account lose another
instant. We cannot spare time even to discuss what you have told
me. You must get home instantly and act."
"What shall I do?"
"There is but one thing to do. It must be done at once. You must
put this piece of paper which you have shown us into the brass
box which you have described. You must also put in a note to say
that all the other papers were burned by your uncle, and that
this is the only one which remains. You must assert that in such
words as will carry conviction with them. Having done this, you
must at once put the box out upon the sundial, as directed. Do
you understand?"
"Entirely."
"Do not think of revenge, or anything of the sort, at present. I
think that we may gain that by means of the law; but we have our
web to weave, while theirs is already woven. The first
consideration is to remove the pressing danger which threatens
you. The second is to clear up the mystery and to punish the
guilty parties."
"I thank you," said the young man, rising and pulling on his
overcoat. "You have given me fresh life and hope. I shall
certainly do as you advise."
"Do not lose an instant. And, above all, take care of yourself in
the meanwhile, for I do not think that there can be a doubt that
you are threatened by a very real and imminent danger. How do you
go back?"
"By train from Waterloo."
"It is not yet nine. The streets will be crowded, so I trust that
you may be in safety. And yet you cannot guard yourself too
closely."
"I am armed."
"That is well. To-morrow I shall set to work upon your case."
"I shall see you at Horsham, then?"
"No, your secret lies in London. It is there that I shall seek
it."
"Then I shall call upon you in a day, or in two days, with news
as to the box and the papers. I shall take your advice in every
particular." He shook hands with us and took his leave. Outside
the wind still screamed and the rain splashed and pattered
against the windows. This strange, wild story seemed to have come
to us from amid the mad elements--blown in upon us like a sheet
of sea-weed in a gale--and now to have been reabsorbed by them
once more.
Sherlock Holmes sat for some time in silence, with his head sunk
forward and his eyes bent upon the red glow of the fire. Then he
lit his pipe, and leaning back in his chair he watched the blue
smoke-rings as they chased each other up to the ceiling.
"I think, Watson," he remarked at last, "that of all our cases we
have had none more fantastic than this."
"Save, perhaps, the Sign of Four."
"Well, yes. Save, perhaps, that. And yet this John Openshaw seems
to me to be walking amid even greater perils than did the
Sholtos."
"But have you," I asked, "formed any definite conception as to
what these perils are?"
"There can be no question as to their nature," he answered.
"Then what are they? Who is this K. K. K., and why does he pursue
this unhappy family?"
Sherlock Holmes closed his eyes and placed his elbows upon the
arms of his chair, with his finger-tips together. "The ideal
reasoner," he remarked, "would, when he had once been shown a
single fact in all its bearings, deduce from it not only all the
chain of events which led up to it but also all the results which
would follow from it. As Cuvier could correctly describe a whole
animal by the contemplation of a single bone, so the observer who
has thoroughly understood one link in a series of incidents
should be able to accurately state all the other ones, both
before and after. We have not yet grasped the results which the
reason alone can attain to. Problems may be solved in the study
which have baffled all those who have sought a solution by the
aid of their senses. To carry the art, however, to its highest
pitch, it is necessary that the reasoner should be able to
utilise all the facts which have come to his knowledge; and this
in itself implies, as you will readily see, a possession of all
knowledge, which, even in these days of free education and
encyclopaedias, is a somewhat rare accomplishment. It is not so
impossible, however, that a man should possess all knowledge
which is likely to be useful to him in his work, and this I have
endeavoured in my case to do. If I remember rightly, you on one
occasion, in the early days of our friendship, defined my limits
in a very precise fashion."
"Yes," I answered, laughing. "It was a singular document.
Philosophy, astronomy, and politics were marked at zero, I
remember. Botany variable, geology profound as regards the
mud-stains from any region within fifty miles of town, chemistry
eccentric, anatomy unsystematic, sensational literature and crime
records unique, violin-player, boxer, swordsman, lawyer, and
self-poisoner by cocaine and tobacco. Those, I think, were the
main points of my analysis."
Holmes grinned at the last item. "Well," he said, "I say now, as
I said then, that a man should keep his little brain-attic
stocked with all the furniture that he is likely to use, and the
rest he can put away in the lumber-room of his library, where he
can get it if he wants it. Now, for such a case as the one which
has been submitted to us to-night, we need certainly to muster
all our resources. Kindly hand me down the letter K of the
'American Encyclopaedia' which stands upon the shelf beside you.
Thank you. Now let us consider the situation and see what may be
deduced from it. In the first place, we may start with a strong
presumption that Colonel Openshaw had some very strong reason for
leaving America. Men at his time of life do not change all their
habits and exchange willingly the charming climate of Florida for
the lonely life of an English provincial town. His extreme love
of solitude in England suggests the idea that he was in fear of
someone or something, so we may assume as a working hypothesis
that it was fear of someone or something which drove him from
America. As to what it was he feared, we can only deduce that by
considering the formidable letters which were received by himself
and his successors. Did you remark the postmarks of those
letters?"
"The first was from Pondicherry, the second from Dundee, and the
third from London."
"From East London. What do you deduce from that?"
"They are all seaports. That the writer was on board of a ship."
"Excellent. We have already a clue. There can be no doubt that
the probability--the strong probability--is that the writer was
on board of a ship. And now let us consider another point. In the
case of Pondicherry, seven weeks elapsed between the threat and
its fulfilment, in Dundee it was only some three or four days.
Does that suggest anything?"
"A greater distance to travel."
"But the letter had also a greater distance to come."
"Then I do not see the point."
"There is at least a presumption that the vessel in which the man
or men are is a sailing-ship. It looks as if they always send
their singular warning or token before them when starting upon
their mission. You see how quickly the deed followed the sign
when it came from Dundee. If they had come from Pondicherry in a
steamer they would have arrived almost as soon as their letter.
But, as a matter of fact, seven weeks elapsed. I think that those
seven weeks represented the difference between the mail-boat which
brought the letter and the sailing vessel which brought the
writer."
"It is possible."
"More than that. It is probable. And now you see the deadly
urgency of this new case, and why I urged young Openshaw to
caution. The blow has always fallen at the end of the time which
it would take the senders to travel the distance. But this one
comes from London, and therefore we cannot count upon delay."
"Good God!" I cried. "What can it mean, this relentless
persecution?"
"The papers which Openshaw carried are obviously of vital
importance to the person or persons in the sailing-ship. I think
that it is quite clear that there must be more than one of them.
A single man could not have carried out two deaths in such a way
as to deceive a coroner's jury. There must have been several in
it, and they must have been men of resource and determination.
Their papers they mean to have, be the holder of them who it may.
In this way you see K. K. K. ceases to be the initials of an
individual and becomes the badge of a society."
"But of what society?"
"Have you never--" said Sherlock Holmes, bending forward and
sinking his voice--"have you never heard of the Ku Klux Klan?"
"I never have."
Holmes turned over the leaves of the book upon his knee. "Here it
is," said he presently:
"'Ku Klux Klan. A name derived from the fanciful resemblance to
the sound produced by cocking a rifle. This terrible secret
society was formed by some ex-Confederate soldiers in the
Southern states after the Civil War, and it rapidly formed local
branches in different parts of the country, notably in Tennessee,
Louisiana, the Carolinas, Georgia, and Florida. Its power was
used for political purposes, principally for the terrorising of
the negro voters and the murdering and driving from the country
of those who were opposed to its views. Its outrages were usually
preceded by a warning sent to the marked man in some fantastic
but generally recognised shape--a sprig of oak-leaves in some
parts, melon seeds or orange pips in others. On receiving this
the victim might either openly abjure his former ways, or might
fly from the country. If he braved the matter out, death would
unfailingly come upon him, and usually in some strange and
unforeseen manner. So perfect was the organisation of the
society, and so systematic its methods, that there is hardly a
case upon record where any man succeeded in braving it with
impunity, or in which any of its outrages were traced home to the
perpetrators. For some years the organisation flourished in spite
of the efforts of the United States government and of the better
classes of the community in the South. Eventually, in the year
1869, the movement rather suddenly collapsed, although there have
been sporadic outbreaks of the same sort since that date.'
"You will observe," said Holmes, laying down the volume, "that
the sudden breaking up of the society was coincident with the
disappearance of Openshaw from America with their papers. It may
well have been cause and effect. It is no wonder that he and his
family have some of the more implacable spirits upon their track.
You can understand that this register and diary may implicate
some of the first men in the South, and that there may be many
who will not sleep easy at night until it is recovered."
"Then the page we have seen--"
"Is such as we might expect. It ran, if I remember right, 'sent
the pips to A, B, and C'--that is, sent the society's warning to
them. Then there are successive entries that A and B cleared, or
left the country, and finally that C was visited, with, I fear, a
sinister result for C. Well, I think, Doctor, that we may let
some light into this dark place, and I believe that the only
chance young Openshaw has in the meantime is to do what I have
told him. There is nothing more to be said or to be done
to-night, so hand me over my violin and let us try to forget for
half an hour the miserable weather and the still more miserable
ways of our fellow-men."
It had cleared in the morning, and the sun was shining with a
subdued brightness through the dim veil which hangs over the
great city. Sherlock Holmes was already at breakfast when I came
down.
"You will excuse me for not waiting for you," said he; "I have, I
foresee, a very busy day before me in looking into this case of
young Openshaw's."
"What steps will you take?" I asked.
"It will very much depend upon the results of my first inquiries.
I may have to go down to Horsham, after all."
"You will not go there first?"
"No, I shall commence with the City. Just ring the bell and the
maid will bring up your coffee."
As I waited, I lifted the unopened newspaper from the table and
glanced my eye over it. It rested upon a heading which sent a
chill to my heart.
"Holmes," I cried, "you are too late."
"Ah!" said he, laying down his cup, "I feared as much. How was it
done?" He spoke calmly, but I could see that he was deeply moved.
"My eye caught the name of Openshaw, and the heading 'Tragedy
Near Waterloo Bridge.' Here is the account:
"Between nine and ten last night Police-Constable Cook, of the H
Division, on duty near Waterloo Bridge, heard a cry for help and
a splash in the water. The night, however, was extremely dark and
stormy, so that, in spite of the help of several passers-by, it
was quite impossible to effect a rescue. The alarm, however, was
given, and, by the aid of the water-police, the body was
eventually recovered. It proved to be that of a young gentleman
whose name, as it appears from an envelope which was found in his
pocket, was John Openshaw, and whose residence is near Horsham.
It is conjectured that he may have been hurrying down to catch
the last train from Waterloo Station, and that in his haste and
the extreme darkness he missed his path and walked over the edge
of one of the small landing-places for river steamboats. The body
exhibited no traces of violence, and there can be no doubt that
the deceased had been the victim of an unfortunate accident,
which should have the effect of calling the attention of the
authorities to the condition of the riverside landing-stages."
We sat in silence for some minutes, Holmes more depressed and
shaken than I had ever seen him.
"That hurts my pride, Watson," he said at last. "It is a petty
feeling, no doubt, but it hurts my pride. It becomes a personal
matter with me now, and, if God sends me health, I shall set my
hand upon this gang. That he should come to me for help, and that
I should send him away to his death--!" He sprang from his chair
and paced about the room in uncontrollable agitation, with a
flush upon his sallow cheeks and a nervous clasping and
unclasping of his long thin hands.
"They must be cunning devils," he exclaimed at last. "How could
they have decoyed him down there? The Embankment is not on the
direct line to the station. The bridge, no doubt, was too
crowded, even on such a night, for their purpose. Well, Watson,
we shall see who will win in the long run. I am going out now!"
"To the police?"
"No; I shall be my own police. When I have spun the web they may
take the flies, but not before."
All day I was engaged in my professional work, and it was late in
the evening before I returned to Baker Street. Sherlock Holmes
had not come back yet. It was nearly ten o'clock before he
entered, looking pale and worn. He walked up to the sideboard,
and tearing a piece from the loaf he devoured it voraciously,
washing it down with a long draught of water.
"You are hungry," I remarked.
"Starving. It had escaped my memory. I have had nothing since
breakfast."
"Nothing?"
"Not a bite. I had no time to think of it."
"And how have you succeeded?"
"Well."
"You have a clue?"
"I have them in the hollow of my hand. Young Openshaw shall not
long remain unavenged. Why, Watson, let us put their own devilish
trade-mark upon them. It is well thought of!"
"What do you mean?"
He took an orange from the cupboard, and tearing it to pieces he
squeezed out the pips upon the table. Of these he took five and
thrust them into an envelope. On the inside of the flap he wrote
"S. H. for J. O." Then he sealed it and addressed it to "Captain
James Calhoun, Barque 'Lone Star,' Savannah, Georgia."
"That will await him when he enters port," said he, chuckling.
"It may give him a sleepless night. He will find it as sure a
precursor of his fate as Openshaw did before him."
"And who is this Captain Calhoun?"
"The leader of the gang. I shall have the others, but he first."
"How did you trace it, then?"
He took a large sheet of paper from his pocket, all covered with
dates and names.
"I have spent the whole day," said he, "over Lloyd's registers
and files of the old papers, following the future career of every
vessel which touched at Pondicherry in January and February in
'83. There were thirty-six ships of fair tonnage which were
reported there during those months. Of these, one, the 'Lone Star,'
instantly attracted my attention, since, although it was reported
as having cleared from London, the name is that which is given to
one of the states of the Union."
"Texas, I think."
"I was not and am not sure which; but I knew that the ship must
have an American origin."
"What then?"
"I searched the Dundee records, and when I found that the barque
'Lone Star' was there in January, '85, my suspicion became a
certainty. I then inquired as to the vessels which lay at present
in the port of London."
"Yes?"
"The 'Lone Star' had arrived here last week. I went down to the
Albert Dock and found that she had been taken down the river by
the early tide this morning, homeward bound to Savannah. I wired
to Gravesend and learned that she had passed some time ago, and
as the wind is easterly I have no doubt that she is now past the
Goodwins and not very far from the Isle of Wight."
"What will you do, then?"
"Oh, I have my hand upon him. He and the two mates, are as I
learn, the only native-born Americans in the ship. The others are
Finns and Germans. I know, also, that they were all three away
from the ship last night. I had it from the stevedore who has
been loading their cargo. By the time that their sailing-ship
reaches Savannah the mail-boat will have carried this letter, and
the cable will have informed the police of Savannah that these
three gentlemen are badly wanted here upon a charge of murder."
There is ever a flaw, however, in the best laid of human plans,
and the murderers of John Openshaw were never to receive the
orange pips which would show them that another, as cunning and as
resolute as themselves, was upon their track. Very long and very
severe were the equinoctial gales that year. We waited long for
news of the "Lone Star" of Savannah, but none ever reached us. We
did at last hear that somewhere far out in the Atlantic a
shattered stern-post of a boat was seen swinging in the trough
of a wave, with the letters "L. S." carved upon it, and that is
all which we shall ever know of the fate of the "Lone Star."
ADVENTURE VI. THE MAN WITH THE TWISTED LIP
Isa Whitney, brother of the late Elias Whitney, D.D., Principal
of the Theological College of St. George's, was much addicted to
opium. The habit grew upon him, as I understand, from some
foolish freak when he was at college; for having read De
Quincey's description of his dreams and sensations, he had
drenched his tobacco with laudanum in an attempt to produce the
same effects. He found, as so many more have done, that the
practice is easier to attain than to get rid of, and for many
years he continued to be a slave to the drug, an object of
mingled horror and pity to his friends and relatives. I can see
him now, with yellow, pasty face, drooping lids, and pin-point
pupils, all huddled in a chair, the wreck and ruin of a noble
man.
One night--it was in June, '89--there came a ring to my bell,
about the hour when a man gives his first yawn and glances at the
clock. I sat up in my chair, and my wife laid her needle-work
down in her lap and made a little face of disappointment.
"A patient!" said she. "You'll have to go out."
I groaned, for I was newly come back from a weary day.
We heard the door open, a few hurried words, and then quick steps
upon the linoleum. Our own door flew open, and a lady, clad in
some dark-coloured stuff, with a black veil, entered the room.
"You will excuse my calling so late," she began, and then,
suddenly losing her self-control, she ran forward, threw her arms
about my wife's neck, and sobbed upon her shoulder. "Oh, I'm in
such trouble!" she cried; "I do so want a little help."
"Why," said my wife, pulling up her veil, "it is Kate Whitney.
How you startled me, Kate! I had not an idea who you were when
you came in."
"I didn't know what to do, so I came straight to you." That was
always the way. Folk who were in grief came to my wife like birds
to a light-house.
"It was very sweet of you to come. Now, you must have some wine
and water, and sit here comfortably and tell us all about it. Or
should you rather that I sent James off to bed?"
"Oh, no, no! I want the doctor's advice and help, too. It's about
Isa. He has not been home for two days. I am so frightened about
him!"
It was not the first time that she had spoken to us of her
husband's trouble, to me as a doctor, to my wife as an old friend
and school companion. We soothed and comforted her by such words
as we could find. Did she know where her husband was? Was it
possible that we could bring him back to her?
It seems that it was. She had the surest information that of late
he had, when the fit was on him, made use of an opium den in the
farthest east of the City. Hitherto his orgies had always been
confined to one day, and he had come back, twitching and
shattered, in the evening. But now the spell had been upon him
eight-and-forty hours, and he lay there, doubtless among the
dregs of the docks, breathing in the poison or sleeping off the
effects. There he was to be found, she was sure of it, at the Bar
of Gold, in Upper Swandam Lane. But what was she to do? How could
she, a young and timid woman, make her way into such a place and
pluck her husband out from among the ruffians who surrounded him?
There was the case, and of course there was but one way out of
it. Might I not escort her to this place? And then, as a second
thought, why should she come at all? I was Isa Whitney's medical
adviser, and as such I had influence over him. I could manage it
better if I were alone. I promised her on my word that I would
send him home in a cab within two hours if he were indeed at the
address which she had given me. And so in ten minutes I had left
my armchair and cheery sitting-room behind me, and was speeding
eastward in a hansom on a strange errand, as it seemed to me at
the time, though the future only could show how strange it was to
be.
But there was no great difficulty in the first stage of my
adventure. Upper Swandam Lane is a vile alley lurking behind the
high wharves which line the north side of the river to the east
of London Bridge. Between a slop-shop and a gin-shop, approached
by a steep flight of steps leading down to a black gap like the
mouth of a cave, I found the den of which I was in search.
Ordering my cab to wait, I passed down the steps, worn hollow in
the centre by the ceaseless tread of drunken feet; and by the
light of a flickering oil-lamp above the door I found the latch
and made my way into a long, low room, thick and heavy with the
brown opium smoke, and terraced with wooden berths, like the
forecastle of an emigrant ship.
Through the gloom one could dimly catch a glimpse of bodies lying
in strange fantastic poses, bowed shoulders, bent knees, heads
thrown back, and chins pointing upward, with here and there a
dark, lack-lustre eye turned upon the newcomer. Out of the black
shadows there glimmered little red circles of light, now bright,
now faint, as the burning poison waxed or waned in the bowls of
the metal pipes. The most lay silent, but some muttered to
themselves, and others talked together in a strange, low,
monotonous voice, their conversation coming in gushes, and then
suddenly tailing off into silence, each mumbling out his own
thoughts and paying little heed to the words of his neighbour. At
the farther end was a small brazier of burning charcoal, beside
which on a three-legged wooden stool there sat a tall, thin old
man, with his jaw resting upon his two fists, and his elbows upon
his knees, staring into the fire.
As I entered, a sallow Malay attendant had hurried up with a pipe
for me and a supply of the drug, beckoning me to an empty berth.
"Thank you. I have not come to stay," said I. "There is a friend
of mine here, Mr. Isa Whitney, and I wish to speak with him."
There was a movement and an exclamation from my right, and
peering through the gloom, I saw Whitney, pale, haggard, and
unkempt, staring out at me.
"My God! It's Watson," said he. He was in a pitiable state of
reaction, with every nerve in a twitter. "I say, Watson, what
o'clock is it?"
"Nearly eleven."
"Of what day?"
"Of Friday, June 19th."
"Good heavens! I thought it was Wednesday. It is Wednesday. What
d'you want to frighten a chap for?" He sank his face onto his
arms and began to sob in a high treble key.
"I tell you that it is Friday, man. Your wife has been waiting
this two days for you. You should be ashamed of yourself!"
"So I am. But you've got mixed, Watson, for I have only been here
a few hours, three pipes, four pipes--I forget how many. But I'll
go home with you. I wouldn't frighten Kate--poor little Kate.
Give me your hand! Have you a cab?"
"Yes, I have one waiting."
"Then I shall go in it. But I must owe something. Find what I
owe, Watson. I am all off colour. I can do nothing for myself."
I walked down the narrow passage between the double row of
sleepers, holding my breath to keep out the vile, stupefying
fumes of the drug, and looking about for the manager. As I passed
the tall man who sat by the brazier I felt a sudden pluck at my
skirt, and a low voice whispered, "Walk past me, and then look
back at me." The words fell quite distinctly upon my ear. I
glanced down. They could only have come from the old man at my
side, and yet he sat now as absorbed as ever, very thin, very
wrinkled, bent with age, an opium pipe dangling down from between
his knees, as though it had dropped in sheer lassitude from his
fingers. I took two steps forward and looked back. It took all my
self-control to prevent me from breaking out into a cry of
astonishment. He had turned his back so that none could see him
but I. His form had filled out, his wrinkles were gone, the dull
eyes had regained their fire, and there, sitting by the fire and
grinning at my surprise, was none other than Sherlock Holmes. He
made a slight motion to me to approach him, and instantly, as he
turned his face half round to the company once more, subsided
into a doddering, loose-lipped senility.
"Holmes!" I whispered, "what on earth are you doing in this den?"
"As low as you can," he answered; "I have excellent ears. If you
would have the great kindness to get rid of that sottish friend
of yours I should be exceedingly glad to have a little talk with
you."
"I have a cab outside."
"Then pray send him home in it. You may safely trust him, for he
appears to be too limp to get into any mischief. I should
recommend you also to send a note by the cabman to your wife to
say that you have thrown in your lot with me. If you will wait
outside, I shall be with you in five minutes."
It was difficult to refuse any of Sherlock Holmes' requests, for
they were always so exceedingly definite, and put forward with
such a quiet air of mastery. I felt, however, that when Whitney
was once confined in the cab my mission was practically
accomplished; and for the rest, I could not wish anything better
than to be associated with my friend in one of those singular
adventures which were the normal condition of his existence. In a
few minutes I had written my note, paid Whitney's bill, led him
out to the cab, and seen him driven through the darkness. In a
very short time a decrepit figure had emerged from the opium den,
and I was walking down the street with Sherlock Holmes. For two
streets he shuffled along with a bent back and an uncertain foot.
Then, glancing quickly round, he straightened himself out and
burst into a hearty fit of laughter.
"I suppose, Watson," said he, "that you imagine that I have added
opium-smoking to cocaine injections, and all the other little
weaknesses on which you have favoured me with your medical
views."
"I was certainly surprised to find you there."
"But not more so than I to find you."
"I came to find a friend."
"And I to find an enemy."
"An enemy?"
"Yes; one of my natural enemies, or, shall I say, my natural
prey. Briefly, Watson, I am in the midst of a very remarkable
inquiry, and I have hoped to find a clue in the incoherent
ramblings of these sots, as I have done before now. Had I been
recognised in that den my life would not have been worth an
hour's purchase; for I have used it before now for my own
purposes, and the rascally Lascar who runs it has sworn to have
vengeance upon me. There is a trap-door at the back of that
building, near the corner of Paul's Wharf, which could tell some
strange tales of what has passed through it upon the moonless
nights."
"What! You do not mean bodies?"
"Ay, bodies, Watson. We should be rich men if we had 1000 pounds
for every poor devil who has been done to death in that den. It
is the vilest murder-trap on the whole riverside, and I fear that
Neville St. Clair has entered it never to leave it more. But our
trap should be here." He put his two forefingers between his
teeth and whistled shrilly--a signal which was answered by a
similar whistle from the distance, followed shortly by the rattle
of wheels and the clink of horses' hoofs.
"Now, Watson," said Holmes, as a tall dog-cart dashed up through
the gloom, throwing out two golden tunnels of yellow light from
its side lanterns. "You'll come with me, won't you?"
"If I can be of use."
"Oh, a trusty comrade is always of use; and a chronicler still
more so. My room at The Cedars is a double-bedded one."
"The Cedars?"
"Yes; that is Mr. St. Clair's house. I am staying there while I
conduct the inquiry."
"Where is it, then?"
"Near Lee, in Kent. We have a seven-mile drive before us."
"But I am all in the dark."
"Of course you are. You'll know all about it presently. Jump up
here. All right, John; we shall not need you. Here's half a
crown. Look out for me to-morrow, about eleven. Give her her
head. So long, then!"
He flicked the horse with his whip, and we dashed away through
the endless succession of sombre and deserted streets, which
widened gradually, until we were flying across a broad
balustraded bridge, with the murky river flowing sluggishly
beneath us. Beyond lay another dull wilderness of bricks and
mortar, its silence broken only by the heavy, regular footfall of
the policeman, or the songs and shouts of some belated party of
revellers. A dull wrack was drifting slowly across the sky, and a
star or two twinkled dimly here and there through the rifts of
the clouds. Holmes drove in silence, with his head sunk upon his
breast, and the air of a man who is lost in thought, while I sat
beside him, curious to learn what this new quest might be which
seemed to tax his powers so sorely, and yet afraid to break in
upon the current of his thoughts. We had driven several miles,
and were beginning to get to the fringe of the belt of suburban
villas, when he shook himself, shrugged his shoulders, and lit up
his pipe with the air of a man who has satisfied himself that he
is acting for the best.
"You have a grand gift of silence, Watson," said he. "It makes
you quite invaluable as a companion. 'Pon my word, it is a great
thing for me to have someone to talk to, for my own thoughts are
not over-pleasant. I was wondering what I should say to this dear
little woman to-night when she meets me at the door."
"You forget that I know nothing about it."
"I shall just have time to tell you the facts of the case before
we get to Lee. It seems absurdly simple, and yet, somehow I can
get nothing to go upon. There's plenty of thread, no doubt, but I
can't get the end of it into my hand. Now, I'll state the case
clearly and concisely to you, Watson, and maybe you can see a
spark where all is dark to me."
"Proceed, then."
"Some years ago--to be definite, in May, 1884--there came to Lee
a gentleman, Neville St. Clair by name, who appeared to have
plenty of money. He took a large villa, laid out the grounds very
nicely, and lived generally in good style. By degrees he made
friends in the neighbourhood, and in 1887 he married the daughter
of a local brewer, by whom he now has two children. He had no
occupation, but was interested in several companies and went into
town as a rule in the morning, returning by the 5:14 from Cannon
Street every night. Mr. St. Clair is now thirty-seven years of
age, is a man of temperate habits, a good husband, a very
affectionate father, and a man who is popular with all who know
him. I may add that his whole debts at the present moment, as far
as we have been able to ascertain, amount to 88 pounds 10s., while
he has 220 pounds standing to his credit in the Capital and
Counties Bank. There is no reason, therefore, to think that money
troubles have been weighing upon his mind.
"Last Monday Mr. Neville St. Clair went into town rather earlier
than usual, remarking before he started that he had two important
commissions to perform, and that he would bring his little boy
home a box of bricks. Now, by the merest chance, his wife
received a telegram upon this same Monday, very shortly after his
departure, to the effect that a small parcel of considerable
value which she had been expecting was waiting for her at the
offices of the Aberdeen Shipping Company. Now, if you are well up
in your London, you will know that the office of the company is
in Fresno Street, which branches out of Upper Swandam Lane, where
you found me to-night. Mrs. St. Clair had her lunch, started for
the City, did some shopping, proceeded to the company's office,
got her packet, and found herself at exactly 4:35 walking through
Swandam Lane on her way back to the station. Have you followed me
so far?"
"It is very clear."
"If you remember, Monday was an exceedingly hot day, and Mrs. St.
Clair walked slowly, glancing about in the hope of seeing a cab,
as she did not like the neighbourhood in which she found herself.
While she was walking in this way down Swandam Lane, she suddenly
heard an ejaculation or cry, and was struck cold to see her
husband looking down at her and, as it seemed to her, beckoning
to her from a second-floor window. The window was open, and she
distinctly saw his face, which she describes as being terribly
agitated. He waved his hands frantically to her, and then
vanished from the window so suddenly that it seemed to her that
he had been plucked back by some irresistible force from behind.
One singular point which struck her quick feminine eye was that
although he wore some dark coat, such as he had started to town
in, he had on neither collar nor necktie.
"Convinced that something was amiss with him, she rushed down the
steps--for the house was none other than the opium den in which
you found me to-night--and running through the front room she
attempted to ascend the stairs which led to the first floor. At
the foot of the stairs, however, she met this Lascar scoundrel of
whom I have spoken, who thrust her back and, aided by a Dane, who
acts as assistant there, pushed her out into the street. Filled
with the most maddening doubts and fears, she rushed down the
lane and, by rare good-fortune, met in Fresno Street a number of
constables with an inspector, all on their way to their beat. The
inspector and two men accompanied her back, and in spite of the
continued resistance of the proprietor, they made their way to
the room in which Mr. St. Clair had last been seen. There was no
sign of him there. In fact, in the whole of that floor there was
no one to be found save a crippled wretch of hideous aspect, who,
it seems, made his home there. Both he and the Lascar stoutly
swore that no one else had been in the front room during the
afternoon. So determined was their denial that the inspector was
staggered, and had almost come to believe that Mrs. St. Clair had
been deluded when, with a cry, she sprang at a small deal box
which lay upon the table and tore the lid from it. Out there fell
a cascade of children's bricks. It was the toy which he had
promised to bring home.
"This discovery, and the evident confusion which the cripple
showed, made the inspector realise that the matter was serious.
The rooms were carefully examined, and results all pointed to an
abominable crime. The front room was plainly furnished as a
sitting-room and led into a small bedroom, which looked out upon
the back of one of the wharves. Between the wharf and the bedroom
window is a narrow strip, which is dry at low tide but is covered
at high tide with at least four and a half feet of water. The
bedroom window was a broad one and opened from below. On
examination traces of blood were to be seen upon the windowsill,
and several scattered drops were visible upon the wooden floor of
the bedroom. Thrust away behind a curtain in the front room were
all the clothes of Mr. Neville St. Clair, with the exception of
his coat. His boots, his socks, his hat, and his watch--all were
there. There were no signs of violence upon any of these
garments, and there were no other traces of Mr. Neville St.
Clair. Out of the window he must apparently have gone for no
other exit could be discovered, and the ominous bloodstains upon
the sill gave little promise that he could save himself by
swimming, for the tide was at its very highest at the moment of
the tragedy.
"And now as to the villains who seemed to be immediately
implicated in the matter. The Lascar was known to be a man of the
vilest antecedents, but as, by Mrs. St. Clair's story, he was
known to have been at the foot of the stair within a very few
seconds of her husband's appearance at the window, he could
hardly have been more than an accessory to the crime. His defence
was one of absolute ignorance, and he protested that he had no
knowledge as to the doings of Hugh Boone, his lodger, and that he
could not account in any way for the presence of the missing
gentleman's clothes.
"So much for the Lascar manager. Now for the sinister cripple who
lives upon the second floor of the opium den, and who was
certainly the last human being whose eyes rested upon Neville St.
Clair. His name is Hugh Boone, and his hideous face is one which
is familiar to every man who goes much to the City. He is a
professional beggar, though in order to avoid the police
regulations he pretends to a small trade in wax vestas. Some
little distance down Threadneedle Street, upon the left-hand
side, there is, as you may have remarked, a small angle in the
wall. Here it is that this creature takes his daily seat,
cross-legged with his tiny stock of matches on his lap, and as he
is a piteous spectacle a small rain of charity descends into the
greasy leather cap which lies upon the pavement beside him. I
have watched the fellow more than once before ever I thought of
making his professional acquaintance, and I have been surprised
at the harvest which he has reaped in a short time. His
appearance, you see, is so remarkable that no one can pass him
without observing him. A shock of orange hair, a pale face
disfigured by a horrible scar, which, by its contraction, has
turned up the outer edge of his upper lip, a bulldog chin, and a
pair of very penetrating dark eyes, which present a singular
contrast to the colour of his hair, all mark him out from amid
the common crowd of mendicants and so, too, does his wit, for he
is ever ready with a reply to any piece of chaff which may be
thrown at him by the passers-by. This is the man whom we now
learn to have been the lodger at the opium den, and to have been
the last man to see the gentleman of whom we are in quest."
"But a cripple!" said I. "What could he have done single-handed
against a man in the prime of life?"
"He is a cripple in the sense that he walks with a limp; but in
other respects he appears to be a powerful and well-nurtured man.
Surely your medical experience would tell you, Watson, that
weakness in one limb is often compensated for by exceptional
strength in the others."
"Pray continue your narrative."
"Mrs. St. Clair had fainted at the sight of the blood upon the
window, and she was escorted home in a cab by the police, as her
presence could be of no help to them in their investigations.
Inspector Barton, who had charge of the case, made a very careful
examination of the premises, but without finding anything which
threw any light upon the matter. One mistake had been made in not
arresting Boone instantly, as he was allowed some few minutes
during which he might have communicated with his friend the
Lascar, but this fault was soon remedied, and he was seized and
searched, without anything being found which could incriminate
him. There were, it is true, some blood-stains upon his right
shirt-sleeve, but he pointed to his ring-finger, which had been
cut near the nail, and explained that the bleeding came from
there, adding that he had been to the window not long before, and
that the stains which had been observed there came doubtless from
the same source. He denied strenuously having ever seen Mr.
Neville St. Clair and swore that the presence of the clothes in
his room was as much a mystery to him as to the police. As to
Mrs. St. Clair's assertion that she had actually seen her husband
at the window, he declared that she must have been either mad or
dreaming. He was removed, loudly protesting, to the
police-station, while the inspector remained upon the premises in
the hope that the ebbing tide might afford some fresh clue.
"And it did, though they hardly found upon the mud-bank what they
had feared to find. It was Neville St. Clair's coat, and not
Neville St. Clair, which lay uncovered as the tide receded. And
what do you think they found in the pockets?"
"I cannot imagine."
"No, I don't think you would guess. Every pocket stuffed with
pennies and half-pennies--421 pennies and 270 half-pennies. It
was no wonder that it had not been swept away by the tide. But a
human body is a different matter. There is a fierce eddy between
the wharf and the house. It seemed likely enough that the
weighted coat had remained when the stripped body had been sucked
away into the river."
"But I understand that all the other clothes were found in the
room. Would the body be dressed in a coat alone?"
"No, sir, but the facts might be met speciously enough. Suppose
that this man Boone had thrust Neville St. Clair through the
window, there is no human eye which could have seen the deed.
What would he do then? It would of course instantly strike him
that he must get rid of the tell-tale garments. He would seize
the coat, then, and be in the act of throwing it out, when it
would occur to him that it would swim and not sink. He has little
time, for he has heard the scuffle downstairs when the wife tried
to force her way up, and perhaps he has already heard from his
Lascar confederate that the police are hurrying up the street.
There is not an instant to be lost. He rushes to some secret
hoard, where he has accumulated the fruits of his beggary, and he
stuffs all the coins upon which he can lay his hands into the
pockets to make sure of the coat's sinking. He throws it out, and
would have done the same with the other garments had not he heard
the rush of steps below, and only just had time to close the
window when the police appeared."
"It certainly sounds feasible."
"Well, we will take it as a working hypothesis for want of a
better. Boone, as I have told you, was arrested and taken to the
station, but it could not be shown that there had ever before
been anything against him. He had for years been known as a
professional beggar, but his life appeared to have been a very
quiet and innocent one. There the matter stands at present, and
the questions which have to be solved--what Neville St. Clair was
doing in the opium den, what happened to him when there, where is
he now, and what Hugh Boone had to do with his disappearance--are
all as far from a solution as ever. I confess that I cannot
recall any case within my experience which looked at the first
glance so simple and yet which presented such difficulties."
While Sherlock Holmes had been detailing this singular series of
events, we had been whirling through the outskirts of the great
town until the last straggling houses had been left behind, and
we rattled along with a country hedge upon either side of us.
Just as he finished, however, we drove through two scattered
villages, where a few lights still glimmered in the windows.
"We are on the outskirts of Lee," said my companion. "We have
touched on three English counties in our short drive, starting in
Middlesex, passing over an angle of Surrey, and ending in Kent.
See that light among the trees? That is The Cedars, and beside
that lamp sits a woman whose anxious ears have already, I have
little doubt, caught the clink of our horse's feet."
"But why are you not conducting the case from Baker Street?" I
asked.
"Because there are many inquiries which must be made out here.
Mrs. St. Clair has most kindly put two rooms at my disposal, and
you may rest assured that she will have nothing but a welcome for
my friend and colleague. I hate to meet her, Watson, when I have
no news of her husband. Here we are. Whoa, there, whoa!"
We had pulled up in front of a large villa which stood within its
own grounds. A stable-boy had run out to the horse's head, and
springing down, I followed Holmes up the small, winding
gravel-drive which led to the house. As we approached, the door
flew open, and a little blonde woman stood in the opening, clad
in some sort of light mousseline de soie, with a touch of fluffy
pink chiffon at her neck and wrists. She stood with her figure
outlined against the flood of light, one hand upon the door, one
half-raised in her eagerness, her body slightly bent, her head
and face protruded, with eager eyes and parted lips, a standing
question.
"Well?" she cried, "well?" And then, seeing that there were two
of us, she gave a cry of hope which sank into a groan as she saw
that my companion shook his head and shrugged his shoulders.
"No good news?"
"None."
"No bad?"
"No."
"Thank God for that. But come in. You must be weary, for you have
had a long day."
"This is my friend, Dr. Watson. He has been of most vital use to
me in several of my cases, and a lucky chance has made it
possible for me to bring him out and associate him with this
investigation."
"I am delighted to see you," said she, pressing my hand warmly.
"You will, I am sure, forgive anything that may be wanting in our
arrangements, when you consider the blow which has come so
suddenly upon us."
"My dear madam," said I, "I am an old campaigner, and if I were
not I can very well see that no apology is needed. If I can be of
any assistance, either to you or to my friend here, I shall be
indeed happy."
"Now, Mr. Sherlock Holmes," said the lady as we entered a
well-lit dining-room, upon the table of which a cold supper had
been laid out, "I should very much like to ask you one or two
plain questions, to which I beg that you will give a plain
answer."
"Certainly, madam."
"Do not trouble about my feelings. I am not hysterical, nor given
to fainting. I simply wish to hear your real, real opinion."
"Upon what point?"
"In your heart of hearts, do you think that Neville is alive?"
Sherlock Holmes seemed to be embarrassed by the question.
"Frankly, now!" she repeated, standing upon the rug and looking
keenly down at him as he leaned back in a basket-chair.
"Frankly, then, madam, I do not."
"You think that he is dead?"
"I do."
"Murdered?"
"I don't say that. Perhaps."
"And on what day did he meet his death?"
"On Monday."
"Then perhaps, Mr. Holmes, you will be good enough to explain how
it is that I have received a letter from him to-day."
Sherlock Holmes sprang out of his chair as if he had been
galvanised.
"What!" he roared.
"Yes, to-day." She stood smiling, holding up a little slip of
paper in the air.
"May I see it?"
"Certainly."
He snatched it from her in his eagerness, and smoothing it out
upon the table he drew over the lamp and examined it intently. I
had left my chair and was gazing at it over his shoulder. The
envelope was a very coarse one and was stamped with the Gravesend
postmark and with the date of that very day, or rather of the day
before, for it was considerably after midnight.
"Coarse writing," murmured Holmes. "Surely this is not your
husband's writing, madam."
"No, but the enclosure is."
"I perceive also that whoever addressed the envelope had to go
and inquire as to the address."
"How can you tell that?"
"The name, you see, is in perfectly black ink, which has dried
itself. The rest is of the greyish colour, which shows that
blotting-paper has been used. If it had been written straight
off, and then blotted, none would be of a deep black shade. This
man has written the name, and there has then been a pause before
he wrote the address, which can only mean that he was not
familiar with it. It is, of course, a trifle, but there is
nothing so important as trifles. Let us now see the letter. Ha!
there has been an enclosure here!"
"Yes, there was a ring. His signet-ring."
"And you are sure that this is your husband's hand?"
"One of his hands."
"One?"
"His hand when he wrote hurriedly. It is very unlike his usual
writing, and yet I know it well."
"'Dearest do not be frightened. All will come well. There is a
huge error which it may take some little time to rectify.
Wait in patience.--NEVILLE.' Written in pencil upon the fly-leaf
of a book, octavo size, no water-mark. Hum! Posted to-day in
Gravesend by a man with a dirty thumb. Ha! And the flap has been
gummed, if I am not very much in error, by a person who had been
chewing tobacco. And you have no doubt that it is your husband's
hand, madam?"
"None. Neville wrote those words."
"And they were posted to-day at Gravesend. Well, Mrs. St. Clair,
the clouds lighten, though I should not venture to say that the
danger is over."
"But he must be alive, Mr. Holmes."
"Unless this is a clever forgery to put us on the wrong scent.
The ring, after all, proves nothing. It may have been taken from
him."
"No, no; it is, it is his very own writing!"
"Very well. It may, however, have been written on Monday and only
posted to-day."
"That is possible."
"If so, much may have happened between."
"Oh, you must not discourage me, Mr. Holmes. I know that all is
well with him. There is so keen a sympathy between us that I
should know if evil came upon him. On the very day that I saw him
last he cut himself in the bedroom, and yet I in the dining-room
rushed upstairs instantly with the utmost certainty that
something had happened. Do you think that I would respond to such
a trifle and yet be ignorant of his death?"
"I have seen too much not to know that the impression of a woman
may be more valuable than the conclusion of an analytical
reasoner. And in this letter you certainly have a very strong
piece of evidence to corroborate your view. But if your husband
is alive and able to write letters, why should he remain away
from you?"
"I cannot imagine. It is unthinkable."
"And on Monday he made no remarks before leaving you?"
"No."
"And you were surprised to see him in Swandam Lane?"
"Very much so."
"Was the window open?"
"Yes."
"Then he might have called to you?"
"He might."
"He only, as I understand, gave an inarticulate cry?"
"Yes."
"A call for help, you thought?"
"Yes. He waved his hands."
"But it might have been a cry of surprise. Astonishment at the
unexpected sight of you might cause him to throw up his hands?"
"It is possible."
"And you thought he was pulled back?"
"He disappeared so suddenly."
"He might have leaped back. You did not see anyone else in the
room?"
"No, but this horrible man confessed to having been there, and
the Lascar was at the foot of the stairs."
"Quite so. Your husband, as far as you could see, had his
ordinary clothes on?"
"But without his collar or tie. I distinctly saw his bare
throat."
"Had he ever spoken of Swandam Lane?"
"Never."
"Had he ever showed any signs of having taken opium?"
"Never."
"Thank you, Mrs. St. Clair. Those are the principal points about
which I wished to be absolutely clear. We shall now have a little
supper and then retire, for we may have a very busy day
to-morrow."
A large and comfortable double-bedded room had been placed at our
disposal, and I was quickly between the sheets, for I was weary
after my night of adventure. Sherlock Holmes was a man, however,
who, when he had an unsolved problem upon his mind, would go for
days, and even for a week, without rest, turning it over,
rearranging his facts, looking at it from every point of view
until he had either fathomed it or convinced himself that his
data were insufficient. It was soon evident to me that he was now
preparing for an all-night sitting. He took off his coat and
waistcoat, put on a large blue dressing-gown, and then wandered
about the room collecting pillows from his bed and cushions from
the sofa and armchairs. With these he constructed a sort of
Eastern divan, upon which he perched himself cross-legged, with
an ounce of shag tobacco and a box of matches laid out in front
of him. In the dim light of the lamp I saw him sitting there, an
old briar pipe between his lips, his eyes fixed vacantly upon the
corner of the ceiling, the blue smoke curling up from him,
silent, motionless, with the light shining upon his strong-set
aquiline features. So he sat as I dropped off to sleep, and so he
sat when a sudden ejaculation caused me to wake up, and I found
the summer sun shining into the apartment. The pipe was still
between his lips, the smoke still curled upward, and the room was
full of a dense tobacco haze, but nothing remained of the heap of
shag which I had seen upon the previous night.
"Awake, Watson?" he asked.
"Yes."
"Game for a morning drive?"
"Certainly."
"Then dress. No one is stirring yet, but I know where the
stable-boy sleeps, and we shall soon have the trap out." He
chuckled to himself as he spoke, his eyes twinkled, and he seemed
a different man to the sombre thinker of the previous night.
As I dressed I glanced at my watch. It was no wonder that no one
was stirring. It was twenty-five minutes past four. I had hardly
finished when Holmes returned with the news that the boy was
putting in the horse.
"I want to test a little theory of mine," said he, pulling on his
boots. "I think, Watson, that you are now standing in the
presence of one of the most absolute fools in Europe. I deserve
to be kicked from here to Charing Cross. But I think I have the
key of the affair now."
"And where is it?" I asked, smiling.
"In the bathroom," he answered. "Oh, yes, I am not joking," he
continued, seeing my look of incredulity. "I have just been
there, and I have taken it out, and I have got it in this
Gladstone bag. Come on, my boy, and we shall see whether it will
not fit the lock."
We made our way downstairs as quietly as possible, and out into
the bright morning sunshine. In the road stood our horse and
trap, with the half-clad stable-boy waiting at the head. We both
sprang in, and away we dashed down the London Road. A few country
carts were stirring, bearing in vegetables to the metropolis, but
the lines of villas on either side were as silent and lifeless as
some city in a dream.
"It has been in some points a singular case," said Holmes,
flicking the horse on into a gallop. "I confess that I have been
as blind as a mole, but it is better to learn wisdom late than
never to learn it at all."
In town the earliest risers were just beginning to look sleepily
from their windows as we drove through the streets of the Surrey
side. Passing down the Waterloo Bridge Road we crossed over the
river, and dashing up Wellington Street wheeled sharply to the
right and found ourselves in Bow Street. Sherlock Holmes was well
known to the force, and the two constables at the door saluted
him. One of them held the horse's head while the other led us in.
"Who is on duty?" asked Holmes.
"Inspector Bradstreet, sir."
"Ah, Bradstreet, how are you?" A tall, stout official had come
down the stone-flagged passage, in a peaked cap and frogged
jacket. "I wish to have a quiet word with you, Bradstreet."
"Certainly, Mr. Holmes. Step into my room here." It was a small,
office-like room, with a huge ledger upon the table, and a
telephone projecting from the wall. The inspector sat down at his
desk.
"What can I do for you, Mr. Holmes?"
"I called about that beggarman, Boone--the one who was charged
with being concerned in the disappearance of Mr. Neville St.
Clair, of Lee."
"Yes. He was brought up and remanded for further inquiries."
"So I heard. You have him here?"
"In the cells."
"Is he quiet?"
"Oh, he gives no trouble. But he is a dirty scoundrel."
"Dirty?"
"Yes, it is all we can do to make him wash his hands, and his
face is as black as a tinker's. Well, when once his case has been
settled, he will have a regular prison bath; and I think, if you
saw him, you would agree with me that he needed it."
"I should like to see him very much."
"Would you? That is easily done. Come this way. You can leave
your bag."
"No, I think that I'll take it."
"Very good. Come this way, if you please." He led us down a
passage, opened a barred door, passed down a winding stair, and
brought us to a whitewashed corridor with a line of doors on each
side.
"The third on the right is his," said the inspector. "Here it
is!" He quietly shot back a panel in the upper part of the door
and glanced through.
"He is asleep," said he. "You can see him very well."
We both put our eyes to the grating. The prisoner lay with his
face towards us, in a very deep sleep, breathing slowly and
heavily. He was a middle-sized man, coarsely clad as became his
calling, with a coloured shirt protruding through the rent in his
tattered coat. He was, as the inspector had said, extremely
dirty, but the grime which covered his face could not conceal its
repulsive ugliness. A broad wheal from an old scar ran right
across it from eye to chin, and by its contraction had turned up
one side of the upper lip, so that three teeth were exposed in a
perpetual snarl. A shock of very bright red hair grew low over
his eyes and forehead.
"He's a beauty, isn't he?" said the inspector.
"He certainly needs a wash," remarked Holmes. "I had an idea that
he might, and I took the liberty of bringing the tools with me."
He opened the Gladstone bag as he spoke, and took out, to my
astonishment, a very large bath-sponge.
"He! he! You are a funny one," chuckled the inspector.
"Now, if you will have the great goodness to open that door very
quietly, we will soon make him cut a much more respectable
figure."
"Well, I don't know why not," said the inspector. "He doesn't
look a credit to the Bow Street cells, does he?" He slipped his
key into the lock, and we all very quietly entered the cell. The
sleeper half turned, and then settled down once more into a deep
slumber. Holmes stooped to the water-jug, moistened his sponge,
and then rubbed it twice vigorously across and down the
prisoner's face.
"Let me introduce you," he shouted, "to Mr. Neville St. Clair, of
Lee, in the county of Kent."
Never in my life have I seen such a sight. The man's face peeled
off under the sponge like the bark from a tree. Gone was the
coarse brown tint! Gone, too, was the horrid scar which had
seamed it across, and the twisted lip which had given the
repulsive sneer to the face! A twitch brought away the tangled
red hair, and there, sitting up in his bed, was a pale,
sad-faced, refined-looking man, black-haired and smooth-skinned,
rubbing his eyes and staring about him with sleepy bewilderment.
Then suddenly realising the exposure, he broke into a scream and
threw himself down with his face to the pillow.
"Great heavens!" cried the inspector, "it is, indeed, the missing
man. I know him from the photograph."
The prisoner turned with the reckless air of a man who abandons
himself to his destiny. "Be it so," said he. "And pray what am I
charged with?"
"With making away with Mr. Neville St.-- Oh, come, you can't be
charged with that unless they make a case of attempted suicide of
it," said the inspector with a grin. "Well, I have been
twenty-seven years in the force, but this really takes the cake."
"If I am Mr. Neville St. Clair, then it is obvious that no crime
has been committed, and that, therefore, I am illegally
detained."
"No crime, but a very great error has been committed," said
Holmes. "You would have done better to have trusted your wife."
"It was not the wife; it was the children," groaned the prisoner.
"God help me, I would not have them ashamed of their father. My
God! What an exposure! What can I do?"
Sherlock Holmes sat down beside him on the couch and patted him
kindly on the shoulder.
"If you leave it to a court of law to clear the matter up," said
he, "of course you can hardly avoid publicity. On the other hand,
if you convince the police authorities that there is no possible
case against you, I do not know that there is any reason that the
details should find their way into the papers. Inspector
Bradstreet would, I am sure, make notes upon anything which you
might tell us and submit it to the proper authorities. The case
would then never go into court at all."
"God bless you!" cried the prisoner passionately. "I would have
endured imprisonment, ay, even execution, rather than have left
my miserable secret as a family blot to my children.
"You are the first who have ever heard my story. My father was a
schoolmaster in Chesterfield, where I received an excellent
education. I travelled in my youth, took to the stage, and
finally became a reporter on an evening paper in London. One day
my editor wished to have a series of articles upon begging in the
metropolis, and I volunteered to supply them. There was the point
from which all my adventures started. It was only by trying
begging as an amateur that I could get the facts upon which to
base my articles. When an actor I had, of course, learned all the
secrets of making up, and had been famous in the green-room for
my skill. I took advantage now of my attainments. I painted my
face, and to make myself as pitiable as possible I made a good
scar and fixed one side of my lip in a twist by the aid of a
small slip of flesh-coloured plaster. Then with a red head of
hair, and an appropriate dress, I took my station in the business
part of the city, ostensibly as a match-seller but really as a
beggar. For seven hours I plied my trade, and when I returned
home in the evening I found to my surprise that I had received no
less than 26s. 4d.
"I wrote my articles and thought little more of the matter until,
some time later, I backed a bill for a friend and had a writ
served upon me for 25 pounds. I was at my wit's end where to get
the money, but a sudden idea came to me. I begged a fortnight's
grace from the creditor, asked for a holiday from my employers,
and spent the time in begging in the City under my disguise. In
ten days I had the money and had paid the debt.
"Well, you can imagine how hard it was to settle down to arduous
work at 2 pounds a week when I knew that I could earn as much in
a day by smearing my face with a little paint, laying my cap on
the ground, and sitting still. It was a long fight between my
pride and the money, but the dollars won at last, and I threw up
reporting and sat day after day in the corner which I had first
chosen, inspiring pity by my ghastly face and filling my pockets
with coppers. Only one man knew my secret. He was the keeper of a
low den in which I used to lodge in Swandam Lane, where I could
every morning emerge as a squalid beggar and in the evenings
transform myself into a well-dressed man about town. This fellow,
a Lascar, was well paid by me for his rooms, so that I knew that
my secret was safe in his possession.
"Well, very soon I found that I was saving considerable sums of
money. I do not mean that any beggar in the streets of London
could earn 700 pounds a year--which is less than my average
takings--but I had exceptional advantages in my power of making
up, and also in a facility of repartee, which improved by
practice and made me quite a recognised character in the City.
All day a stream of pennies, varied by silver, poured in upon me,
and it was a very bad day in which I failed to take 2 pounds.
"As I grew richer I grew more ambitious, took a house in the
country, and eventually married, without anyone having a
suspicion as to my real occupation. My dear wife knew that I had
business in the City. She little knew what.
"Last Monday I had finished for the day and was dressing in my
room above the opium den when I looked out of my window and saw,
to my horror and astonishment, that my wife was standing in the
street, with her eyes fixed full upon me. I gave a cry of
surprise, threw up my arms to cover my face, and, rushing to my
confidant, the Lascar, entreated him to prevent anyone from
coming up to me. I heard her voice downstairs, but I knew that
she could not ascend. Swiftly I threw off my clothes, pulled on
those of a beggar, and put on my pigments and wig. Even a wife's
eyes could not pierce so complete a disguise. But then it
occurred to me that there might be a search in the room, and that
the clothes might betray me. I threw open the window, reopening
by my violence a small cut which I had inflicted upon myself in
the bedroom that morning. Then I seized my coat, which was
weighted by the coppers which I had just transferred to it from
the leather bag in which I carried my takings. I hurled it out of
the window, and it disappeared into the Thames. The other clothes
would have followed, but at that moment there was a rush of
constables up the stair, and a few minutes after I found, rather,
I confess, to my relief, that instead of being identified as Mr.
Neville St. Clair, I was arrested as his murderer.
"I do not know that there is anything else for me to explain. I
was determined to preserve my disguise as long as possible, and
hence my preference for a dirty face. Knowing that my wife would
be terribly anxious, I slipped off my ring and confided it to the
Lascar at a moment when no constable was watching me, together
with a hurried scrawl, telling her that she had no cause to
fear."
"That note only reached her yesterday," said Holmes.
"Good God! What a week she must have spent!"
"The police have watched this Lascar," said Inspector Bradstreet,
"and I can quite understand that he might find it difficult to
post a letter unobserved. Probably he handed it to some sailor
customer of his, who forgot all about it for some days."
"That was it," said Holmes, nodding approvingly; "I have no doubt
of it. But have you never been prosecuted for begging?"
"Many times; but what was a fine to me?"
"It must stop here, however," said Bradstreet. "If the police are
to hush this thing up, there must be no more of Hugh Boone."
"I have sworn it by the most solemn oaths which a man can take."
"In that case I think that it is probable that no further steps
may be taken. But if you are found again, then all must come out.
I am sure, Mr. Holmes, that we are very much indebted to you for
having cleared the matter up. I wish I knew how you reach your
results."
"I reached this one," said my friend, "by sitting upon five
pillows and consuming an ounce of shag. I think, Watson, that if
we drive to Baker Street we shall just be in time for breakfast."
VII. THE ADVENTURE OF THE BLUE CARBUNCLE
I had called upon my friend Sherlock Holmes upon the second
morning after Christmas, with the intention of wishing him the
compliments of the season. He was lounging upon the sofa in a
purple dressing-gown, a pipe-rack within his reach upon the
right, and a pile of crumpled morning papers, evidently newly
studied, near at hand. Beside the couch was a wooden chair, and
on the angle of the back hung a very seedy and disreputable
hard-felt hat, much the worse for wear, and cracked in several
places. A lens and a forceps lying upon the seat of the chair
suggested that the hat had been suspended in this manner for the
purpose of examination.
"You are engaged," said I; "perhaps I interrupt you."
"Not at all. I am glad to have a friend with whom I can discuss
my results. The matter is a perfectly trivial one"--he jerked his
thumb in the direction of the old hat--"but there are points in
connection with it which are not entirely devoid of interest and
even of instruction."
I seated myself in his armchair and warmed my hands before his
crackling fire, for a sharp frost had set in, and the windows
were thick with the ice crystals. "I suppose," I remarked, "that,
homely as it looks, this thing has some deadly story linked on to
it--that it is the clue which will guide you in the solution of
some mystery and the punishment of some crime."
"No, no. No crime," said Sherlock Holmes, laughing. "Only one of
those whimsical little incidents which will happen when you have
four million human beings all jostling each other within the
space of a few square miles. Amid the action and reaction of so
dense a swarm of humanity, every possible combination of events
may be expected to take place, and many a little problem will be
presented which may be striking and bizarre without being
criminal. We have already had experience of such."
"So much so," I remarked, "that of the last six cases which I
have added to my notes, three have been entirely free of any
legal crime."
"Precisely. You allude to my attempt to recover the Irene Adler
papers, to the singular case of Miss Mary Sutherland, and to the
adventure of the man with the twisted lip. Well, I have no doubt
that this small matter will fall into the same innocent category.
You know Peterson, the commissionaire?"
"Yes."
"It is to him that this trophy belongs."
"It is his hat."
"No, no, he found it. Its owner is unknown. I beg that you will
look upon it not as a battered billycock but as an intellectual
problem. And, first, as to how it came here. It arrived upon
Christmas morning, in company with a good fat goose, which is, I
have no doubt, roasting at this moment in front of Peterson's
fire. The facts are these: about four o'clock on Christmas
morning, Peterson, who, as you know, is a very honest fellow, was
returning from some small jollification and was making his way
homeward down Tottenham Court Road. In front of him he saw, in
the gaslight, a tallish man, walking with a slight stagger, and
carrying a white goose slung over his shoulder. As he reached the
corner of Goodge Street, a row broke out between this stranger
and a little knot of roughs. One of the latter knocked off the
man's hat, on which he raised his stick to defend himself and,
swinging it over his head, smashed the shop window behind him.
Peterson had rushed forward to protect the stranger from his
assailants; but the man, shocked at having broken the window, and
seeing an official-looking person in uniform rushing towards him,
dropped his goose, took to his heels, and vanished amid the
labyrinth of small streets which lie at the back of Tottenham
Court Road. The roughs had also fled at the appearance of
Peterson, so that he was left in possession of the field of
battle, and also of the spoils of victory in the shape of this
battered hat and a most unimpeachable Christmas goose."
"Which surely he restored to their owner?"
"My dear fellow, there lies the problem. It is true that 'For
Mrs. Henry Baker' was printed upon a small card which was tied to
the bird's left leg, and it is also true that the initials 'H.
B.' are legible upon the lining of this hat, but as there are
some thousands of Bakers, and some hundreds of Henry Bakers in
this city of ours, it is not easy to restore lost property to any
one of them."
"What, then, did Peterson do?"
"He brought round both hat and goose to me on Christmas morning,
knowing that even the smallest problems are of interest to me.
The goose we retained until this morning, when there were signs
that, in spite of the slight frost, it would be well that it
should be eaten without unnecessary delay. Its finder has carried
it off, therefore, to fulfil the ultimate destiny of a goose,
while I continue to retain the hat of the unknown gentleman who
lost his Christmas dinner."
"Did he not advertise?"
"No."
"Then, what clue could you have as to his identity?"
"Only as much as we can deduce."
"From his hat?"
"Precisely."
"But you are joking. What can you gather from this old battered
felt?"
"Here is my lens. You know my methods. What can you gather
yourself as to the individuality of the man who has worn this
article?"
I took the tattered object in my hands and turned it over rather
ruefully. It was a very ordinary black hat of the usual round
shape, hard and much the worse for wear. The lining had been of
red silk, but was a good deal discoloured. There was no maker's
name; but, as Holmes had remarked, the initials "H. B." were
scrawled upon one side. It was pierced in the brim for a
hat-securer, but the elastic was missing. For the rest, it was
cracked, exceedingly dusty, and spotted in several places,
although there seemed to have been some attempt to hide the
discoloured patches by smearing them with ink.
"I can see nothing," said I, handing it back to my friend.
"On the contrary, Watson, you can see everything. You fail,
however, to reason from what you see. You are too timid in
drawing your inferences."
"Then, pray tell me what it is that you can infer from this hat?"
He picked it up and gazed at it in the peculiar introspective
fashion which was characteristic of him. "It is perhaps less
suggestive than it might have been," he remarked, "and yet there
are a few inferences which are very distinct, and a few others
which represent at least a strong balance of probability. That
the man was highly intellectual is of course obvious upon the
face of it, and also that he was fairly well-to-do within the
last three years, although he has now fallen upon evil days. He
had foresight, but has less now than formerly, pointing to a
moral retrogression, which, when taken with the decline of his
fortunes, seems to indicate some evil influence, probably drink,
at work upon him. This may account also for the obvious fact that
his wife has ceased to love him."
"My dear Holmes!"
"He has, however, retained some degree of self-respect," he
continued, disregarding my remonstrance. "He is a man who leads a
sedentary life, goes out little, is out of training entirely, is
middle-aged, has grizzled hair which he has had cut within the
last few days, and which he anoints with lime-cream. These are
the more patent facts which are to be deduced from his hat. Also,
by the way, that it is extremely improbable that he has gas laid
on in his house."
"You are certainly joking, Holmes."
"Not in the least. Is it possible that even now, when I give you
these results, you are unable to see how they are attained?"
"I have no doubt that I am very stupid, but I must confess that I
am unable to follow you. For example, how did you deduce that
this man was intellectual?"
For answer Holmes clapped the hat upon his head. It came right
over the forehead and settled upon the bridge of his nose. "It is
a question of cubic capacity," said he; "a man with so large a
brain must have something in it."
"The decline of his fortunes, then?"
"This hat is three years old. These flat brims curled at the edge
came in then. It is a hat of the very best quality. Look at the
band of ribbed silk and the excellent lining. If this man could
afford to buy so expensive a hat three years ago, and has had no
hat since, then he has assuredly gone down in the world."
"Well, that is clear enough, certainly. But how about the
foresight and the moral retrogression?"
Sherlock Holmes laughed. "Here is the foresight," said he putting
his finger upon the little disc and loop of the hat-securer.
"They are never sold upon hats. If this man ordered one, it is a
sign of a certain amount of foresight, since he went out of his
way to take this precaution against the wind. But since we see
that he has broken the elastic and has not troubled to replace
it, it is obvious that he has less foresight now than formerly,
which is a distinct proof of a weakening nature. On the other
hand, he has endeavoured to conceal some of these stains upon the
felt by daubing them with ink, which is a sign that he has not
entirely lost his self-respect."
"Your reasoning is certainly plausible."
"The further points, that he is middle-aged, that his hair is
grizzled, that it has been recently cut, and that he uses
lime-cream, are all to be gathered from a close examination of the
lower part of the lining. The lens discloses a large number of
hair-ends, clean cut by the scissors of the barber. They all
appear to be adhesive, and there is a distinct odour of
lime-cream. This dust, you will observe, is not the gritty, grey
dust of the street but the fluffy brown dust of the house,
showing that it has been hung up indoors most of the time, while
the marks of moisture upon the inside are proof positive that the
wearer perspired very freely, and could therefore, hardly be in
the best of training."
"But his wife--you said that she had ceased to love him."
"This hat has not been brushed for weeks. When I see you, my dear
Watson, with a week's accumulation of dust upon your hat, and
when your wife allows you to go out in such a state, I shall fear
that you also have been unfortunate enough to lose your wife's
affection."
"But he might be a bachelor."
"Nay, he was bringing home the goose as a peace-offering to his
wife. Remember the card upon the bird's leg."
"You have an answer to everything. But how on earth do you deduce
that the gas is not laid on in his house?"
"One tallow stain, or even two, might come by chance; but when I
see no less than five, I think that there can be little doubt
that the individual must be brought into frequent contact with
burning tallow--walks upstairs at night probably with his hat in
one hand and a guttering candle in the other. Anyhow, he never
got tallow-stains from a gas-jet. Are you satisfied?"
"Well, it is very ingenious," said I, laughing; "but since, as
you said just now, there has been no crime committed, and no harm
done save the loss of a goose, all this seems to be rather a
waste of energy."
Sherlock Holmes had opened his mouth to reply, when the door flew
open, and Peterson, the commissionaire, rushed into the apartment
with flushed cheeks and the face of a man who is dazed with
astonishment.
"The goose, Mr. Holmes! The goose, sir!" he gasped.
"Eh? What of it, then? Has it returned to life and flapped off
through the kitchen window?" Holmes twisted himself round upon
the sofa to get a fairer view of the man's excited face.
"See here, sir! See what my wife found in its crop!" He held out
his hand and displayed upon the centre of the palm a brilliantly
scintillating blue stone, rather smaller than a bean in size, but
of such purity and radiance that it twinkled like an electric
point in the dark hollow of his hand.
Sherlock Holmes sat up with a whistle. "By Jove, Peterson!" said
he, "this is treasure trove indeed. I suppose you know what you
have got?"
"A diamond, sir? A precious stone. It cuts into glass as though
it were putty."
"It's more than a precious stone. It is the precious stone."
"Not the Countess of Morcar's blue carbuncle!" I ejaculated.
"Precisely so. I ought to know its size and shape, seeing that I
have read the advertisement about it in The Times every day
lately. It is absolutely unique, and its value can only be
conjectured, but the reward offered of 1000 pounds is certainly
not within a twentieth part of the market price."
"A thousand pounds! Great Lord of mercy!" The commissionaire
plumped down into a chair and stared from one to the other of us.
"That is the reward, and I have reason to know that there are
sentimental considerations in the background which would induce
the Countess to part with half her fortune if she could but
recover the gem."
"It was lost, if I remember aright, at the Hotel Cosmopolitan," I
remarked.
"Precisely so, on December 22nd, just five days ago. John Horner,
a plumber, was accused of having abstracted it from the lady's
jewel-case. The evidence against him was so strong that the case
has been referred to the Assizes. I have some account of the
matter here, I believe." He rummaged amid his newspapers,
glancing over the dates, until at last he smoothed one out,
doubled it over, and read the following paragraph:
"Hotel Cosmopolitan Jewel Robbery. John Horner, 26, plumber, was
brought up upon the charge of having upon the 22nd inst.,
abstracted from the jewel-case of the Countess of Morcar the
valuable gem known as the blue carbuncle. James Ryder,
upper-attendant at the hotel, gave his evidence to the effect
that he had shown Horner up to the dressing-room of the Countess
of Morcar upon the day of the robbery in order that he might
solder the second bar of the grate, which was loose. He had
remained with Horner some little time, but had finally been
called away. On returning, he found that Horner had disappeared,
that the bureau had been forced open, and that the small morocco
casket in which, as it afterwards transpired, the Countess was
accustomed to keep her jewel, was lying empty upon the
dressing-table. Ryder instantly gave the alarm, and Horner was
arrested the same evening; but the stone could not be found
either upon his person or in his rooms. Catherine Cusack, maid to
the Countess, deposed to having heard Ryder's cry of dismay on
discovering the robbery, and to having rushed into the room,
where she found matters as described by the last witness.
Inspector Bradstreet, B division, gave evidence as to the arrest
of Horner, who struggled frantically, and protested his innocence
in the strongest terms. Evidence of a previous conviction for
robbery having been given against the prisoner, the magistrate
refused to deal summarily with the offence, but referred it to
the Assizes. Horner, who had shown signs of intense emotion
during the proceedings, fainted away at the conclusion and was
carried out of court."
"Hum! So much for the police-court," said Holmes thoughtfully,
tossing aside the paper. "The question for us now to solve is the
sequence of events leading from a rifled jewel-case at one end to
the crop of a goose in Tottenham Court Road at the other. You
see, Watson, our little deductions have suddenly assumed a much
more important and less innocent aspect. Here is the stone; the
stone came from the goose, and the goose came from Mr. Henry
Baker, the gentleman with the bad hat and all the other
characteristics with which I have bored you. So now we must set
ourselves very seriously to finding this gentleman and
ascertaining what part he has played in this little mystery. To
do this, we must try the simplest means first, and these lie
undoubtedly in an advertisement in all the evening papers. If
this fail, I shall have recourse to other methods."
"What will you say?"
"Give me a pencil and that slip of paper. Now, then: 'Found at
the corner of Goodge Street, a goose and a black felt hat. Mr.
Henry Baker can have the same by applying at 6:30 this evening at
221B, Baker Street.' That is clear and concise."
"Very. But will he see it?"
"Well, he is sure to keep an eye on the papers, since, to a poor
man, the loss was a heavy one. He was clearly so scared by his
mischance in breaking the window and by the approach of Peterson
that he thought of nothing but flight, but since then he must
have bitterly regretted the impulse which caused him to drop his
bird. Then, again, the introduction of his name will cause him to
see it, for everyone who knows him will direct his attention to
it. Here you are, Peterson, run down to the advertising agency
and have this put in the evening papers."
"In which, sir?"
"Oh, in the Globe, Star, Pall Mall, St. James's, Evening News,
Standard, Echo, and any others that occur to you."
"Very well, sir. And this stone?"
"Ah, yes, I shall keep the stone. Thank you. And, I say,
Peterson, just buy a goose on your way back and leave it here
with me, for we must have one to give to this gentleman in place
of the one which your family is now devouring."
When the commissionaire had gone, Holmes took up the stone and
held it against the light. "It's a bonny thing," said he. "Just
see how it glints and sparkles. Of course it is a nucleus and
focus of crime. Every good stone is. They are the devil's pet
baits. In the larger and older jewels every facet may stand for a
bloody deed. This stone is not yet twenty years old. It was found
in the banks of the Amoy River in southern China and is remarkable
in having every characteristic of the carbuncle, save that it is
blue in shade instead of ruby red. In spite of its youth, it has
already a sinister history. There have been two murders, a
vitriol-throwing, a suicide, and several robberies brought about
for the sake of this forty-grain weight of crystallised charcoal.
Who would think that so pretty a toy would be a purveyor to the
gallows and the prison? I'll lock it up in my strong box now and
drop a line to the Countess to say that we have it."
"Do you think that this man Horner is innocent?"
"I cannot tell."
"Well, then, do you imagine that this other one, Henry Baker, had
anything to do with the matter?"
"It is, I think, much more likely that Henry Baker is an
absolutely innocent man, who had no idea that the bird which he
was carrying was of considerably more value than if it were made
of solid gold. That, however, I shall determine by a very simple
test if we have an answer to our advertisement."
"And you can do nothing until then?"
"Nothing."
"In that case I shall continue my professional round. But I shall
come back in the evening at the hour you have mentioned, for I
should like to see the solution of so tangled a business."
"Very glad to see you. I dine at seven. There is a woodcock, I
believe. By the way, in view of recent occurrences, perhaps I
ought to ask Mrs. Hudson to examine its crop."
I had been delayed at a case, and it was a little after half-past
six when I found myself in Baker Street once more. As I
approached the house I saw a tall man in a Scotch bonnet with a
coat which was buttoned up to his chin waiting outside in the
bright semicircle which was thrown from the fanlight. Just as I
arrived the door was opened, and we were shown up together to
Holmes' room.
"Mr. Henry Baker, I believe," said he, rising from his armchair
and greeting his visitor with the easy air of geniality which he
could so readily assume. "Pray take this chair by the fire, Mr.
Baker. It is a cold night, and I observe that your circulation is
more adapted for summer than for winter. Ah, Watson, you have
just come at the right time. Is that your hat, Mr. Baker?"
"Yes, sir, that is undoubtedly my hat."
He was a large man with rounded shoulders, a massive head, and a
broad, intelligent face, sloping down to a pointed beard of
grizzled brown. A touch of red in nose and cheeks, with a slight
tremor of his extended hand, recalled Holmes' surmise as to his
habits. His rusty black frock-coat was buttoned right up in
front, with the collar turned up, and his lank wrists protruded
from his sleeves without a sign of cuff or shirt. He spoke in a
slow staccato fashion, choosing his words with care, and gave the
impression generally of a man of learning and letters who had had
ill-usage at the hands of fortune.
"We have retained these things for some days," said Holmes,
"because we expected to see an advertisement from you giving your
address. I am at a loss to know now why you did not advertise."
Our visitor gave a rather shamefaced laugh. "Shillings have not
been so plentiful with me as they once were," he remarked. "I had
no doubt that the gang of roughs who assaulted me had carried off
both my hat and the bird. I did not care to spend more money in a
hopeless attempt at recovering them."
"Very naturally. By the way, about the bird, we were compelled to
eat it."
"To eat it!" Our visitor half rose from his chair in his
excitement.
"Yes, it would have been of no use to anyone had we not done so.
But I presume that this other goose upon the sideboard, which is
about the same weight and perfectly fresh, will answer your
purpose equally well?"
"Oh, certainly, certainly," answered Mr. Baker with a sigh of
relief.
"Of course, we still have the feathers, legs, crop, and so on of
your own bird, so if you wish--"
The man burst into a hearty laugh. "They might be useful to me as
relics of my adventure," said he, "but beyond that I can hardly
see what use the disjecta membra of my late acquaintance are
going to be to me. No, sir, I think that, with your permission, I
will confine my attentions to the excellent bird which I perceive
upon the sideboard."
Sherlock Holmes glanced sharply across at me with a slight shrug
of his shoulders.
"There is your hat, then, and there your bird," said he. "By the
way, would it bore you to tell me where you got the other one
from? I am somewhat of a fowl fancier, and I have seldom seen a
better grown goose."
"Certainly, sir," said Baker, who had risen and tucked his newly
gained property under his arm. "There are a few of us who
frequent the Alpha Inn, near the Museum--we are to be found in
the Museum itself during the day, you understand. This year our
good host, Windigate by name, instituted a goose club, by which,
on consideration of some few pence every week, we were each to
receive a bird at Christmas. My pence were duly paid, and the
rest is familiar to you. I am much indebted to you, sir, for a
Scotch bonnet is fitted neither to my years nor my gravity." With
a comical pomposity of manner he bowed solemnly to both of us and
strode off upon his way.
"So much for Mr. Henry Baker," said Holmes when he had closed the
door behind him. "It is quite certain that he knows nothing
whatever about the matter. Are you hungry, Watson?"
"Not particularly."
"Then I suggest that we turn our dinner into a supper and follow
up this clue while it is still hot."
"By all means."
It was a bitter night, so we drew on our ulsters and wrapped
cravats about our throats. Outside, the stars were shining coldly
in a cloudless sky, and the breath of the passers-by blew out
into smoke like so many pistol shots. Our footfalls rang out
crisply and loudly as we swung through the doctors' quarter,
Wimpole Street, Harley Street, and so through Wigmore Street into
Oxford Street. In a quarter of an hour we were in Bloomsbury at
the Alpha Inn, which is a small public-house at the corner of one
of the streets which runs down into Holborn. Holmes pushed open
the door of the private bar and ordered two glasses of beer from
the ruddy-faced, white-aproned landlord.
"Your beer should be excellent if it is as good as your geese,"
said he.
"My geese!" The man seemed surprised.
"Yes. I was speaking only half an hour ago to Mr. Henry Baker,
who was a member of your goose club."
"Ah! yes, I see. But you see, sir, them's not our geese."
"Indeed! Whose, then?"
"Well, I got the two dozen from a salesman in Covent Garden."
"Indeed? I know some of them. Which was it?"
"Breckinridge is his name."
"Ah! I don't know him. Well, here's your good health landlord,
and prosperity to your house. Good-night."
"Now for Mr. Breckinridge," he continued, buttoning up his coat
as we came out into the frosty air. "Remember, Watson that though
we have so homely a thing as a goose at one end of this chain, we
have at the other a man who will certainly get seven years' penal
servitude unless we can establish his innocence. It is possible
that our inquiry may but confirm his guilt; but, in any case, we
have a line of investigation which has been missed by the police,
and which a singular chance has placed in our hands. Let us
follow it out to the bitter end. Faces to the south, then, and
quick march!"
We passed across Holborn, down Endell Street, and so through a
zigzag of slums to Covent Garden Market. One of the largest
stalls bore the name of Breckinridge upon it, and the proprietor
a horsey-looking man, with a sharp face and trim side-whiskers was
helping a boy to put up the shutters.
"Good-evening. It's a cold night," said Holmes.
The salesman nodded and shot a questioning glance at my
companion.
"Sold out of geese, I see," continued Holmes, pointing at the
bare slabs of marble.
"Let you have five hundred to-morrow morning."
"That's no good."
"Well, there are some on the stall with the gas-flare."
"Ah, but I was recommended to you."
"Who by?"
"The landlord of the Alpha."
"Oh, yes; I sent him a couple of dozen."
"Fine birds they were, too. Now where did you get them from?"
To my surprise the question provoked a burst of anger from the
salesman.
"Now, then, mister," said he, with his head cocked and his arms
akimbo, "what are you driving at? Let's have it straight, now."
"It is straight enough. I should like to know who sold you the
geese which you supplied to the Alpha."
"Well then, I shan't tell you. So now!"
"Oh, it is a matter of no importance; but I don't know why you
should be so warm over such a trifle."
"Warm! You'd be as warm, maybe, if you were as pestered as I am.
When I pay good money for a good article there should be an end
of the business; but it's 'Where are the geese?' and 'Who did you
sell the geese to?' and 'What will you take for the geese?' One
would think they were the only geese in the world, to hear the
fuss that is made over them."
"Well, I have no connection with any other people who have been
making inquiries," said Holmes carelessly. "If you won't tell us
the bet is off, that is all. But I'm always ready to back my
opinion on a matter of fowls, and I have a fiver on it that the
bird I ate is country bred."
"Well, then, you've lost your fiver, for it's town bred," snapped
the salesman.
"It's nothing of the kind."
"I say it is."
"I don't believe it."
"D'you think you know more about fowls than I, who have handled
them ever since I was a nipper? I tell you, all those birds that
went to the Alpha were town bred."
"You'll never persuade me to believe that."
"Will you bet, then?"
"It's merely taking your money, for I know that I am right. But
I'll have a sovereign on with you, just to teach you not to be
obstinate."
The salesman chuckled grimly. "Bring me the books, Bill," said
he.
The small boy brought round a small thin volume and a great
greasy-backed one, laying them out together beneath the hanging
lamp.
"Now then, Mr. Cocksure," said the salesman, "I thought that I
was out of geese, but before I finish you'll find that there is
still one left in my shop. You see this little book?"
"Well?"
"That's the list of the folk from whom I buy. D'you see? Well,
then, here on this page are the country folk, and the numbers
after their names are where their accounts are in the big ledger.
Now, then! You see this other page in red ink? Well, that is a
list of my town suppliers. Now, look at that third name. Just
read it out to me."
"Mrs. Oakshott, 117, Brixton Road--249," read Holmes.
"Quite so. Now turn that up in the ledger."
Holmes turned to the page indicated. "Here you are, 'Mrs.
Oakshott, 117, Brixton Road, egg and poultry supplier.'"
"Now, then, what's the last entry?"
"'December 22nd. Twenty-four geese at 7s. 6d.'"
"Quite so. There you are. And underneath?"
"'Sold to Mr. Windigate of the Alpha, at 12s.'"
"What have you to say now?"
Sherlock Holmes looked deeply chagrined. He drew a sovereign from
his pocket and threw it down upon the slab, turning away with the
air of a man whose disgust is too deep for words. A few yards off
he stopped under a lamp-post and laughed in the hearty, noiseless
fashion which was peculiar to him.
"When you see a man with whiskers of that cut and the 'Pink 'un'
protruding out of his pocket, you can always draw him by a bet,"
said he. "I daresay that if I had put 100 pounds down in front of
him, that man would not have given me such complete information
as was drawn from him by the idea that he was doing me on a
wager. Well, Watson, we are, I fancy, nearing the end of our
quest, and the only point which remains to be determined is
whether we should go on to this Mrs. Oakshott to-night, or
whether we should reserve it for to-morrow. It is clear from what
that surly fellow said that there are others besides ourselves
who are anxious about the matter, and I should--"
His remarks were suddenly cut short by a loud hubbub which broke
out from the stall which we had just left. Turning round we saw a
little rat-faced fellow standing in the centre of the circle of
yellow light which was thrown by the swinging lamp, while
Breckinridge, the salesman, framed in the door of his stall, was
shaking his fists fiercely at the cringing figure.
"I've had enough of you and your geese," he shouted. "I wish you
were all at the devil together. If you come pestering me any more
with your silly talk I'll set the dog at you. You bring Mrs.
Oakshott here and I'll answer her, but what have you to do with
it? Did I buy the geese off you?"
"No; but one of them was mine all the same," whined the little
man.
"Well, then, ask Mrs. Oakshott for it."
"She told me to ask you."
"Well, you can ask the King of Proosia, for all I care. I've had
enough of it. Get out of this!" He rushed fiercely forward, and
the inquirer flitted away into the darkness.
"Ha! this may save us a visit to Brixton Road," whispered Holmes.
"Come with me, and we will see what is to be made of this
fellow." Striding through the scattered knots of people who
lounged round the flaring stalls, my companion speedily overtook
the little man and touched him upon the shoulder. He sprang
round, and I could see in the gas-light that every vestige of
colour had been driven from his face.
"Who are you, then? What do you want?" he asked in a quavering
voice.
"You will excuse me," said Holmes blandly, "but I could not help
overhearing the questions which you put to the salesman just now.
I think that I could be of assistance to you."
"You? Who are you? How could you know anything of the matter?"
"My name is Sherlock Holmes. It is my business to know what other
people don't know."
"But you can know nothing of this?"
"Excuse me, I know everything of it. You are endeavouring to
trace some geese which were sold by Mrs. Oakshott, of Brixton
Road, to a salesman named Breckinridge, by him in turn to Mr.
Windigate, of the Alpha, and by him to his club, of which Mr.
Henry Baker is a member."
"Oh, sir, you are the very man whom I have longed to meet," cried
the little fellow with outstretched hands and quivering fingers.
"I can hardly explain to you how interested I am in this matter."
Sherlock Holmes hailed a four-wheeler which was passing. "In that
case we had better discuss it in a cosy room rather than in this
wind-swept market-place," said he. "But pray tell me, before we
go farther, who it is that I have the pleasure of assisting."
The man hesitated for an instant. "My name is John Robinson," he
answered with a sidelong glance.
"No, no; the real name," said Holmes sweetly. "It is always
awkward doing business with an alias."
A flush sprang to the white cheeks of the stranger. "Well then,"
said he, "my real name is James Ryder."
"Precisely so. Head attendant at the Hotel Cosmopolitan. Pray
step into the cab, and I shall soon be able to tell you
everything which you would wish to know."
The little man stood glancing from one to the other of us with
half-frightened, half-hopeful eyes, as one who is not sure
whether he is on the verge of a windfall or of a catastrophe.
Then he stepped into the cab, and in half an hour we were back in
the sitting-room at Baker Street. Nothing had been said during
our drive, but the high, thin breathing of our new companion, and
the claspings and unclaspings of his hands, spoke of the nervous
tension within him.
"Here we are!" said Holmes cheerily as we filed into the room.
"The fire looks very seasonable in this weather. You look cold,
Mr. Ryder. Pray take the basket-chair. I will just put on my
slippers before we settle this little matter of yours. Now, then!
You want to know what became of those geese?"
"Yes, sir."
"Or rather, I fancy, of that goose. It was one bird, I imagine in
which you were interested--white, with a black bar across the
tail."
Ryder quivered with emotion. "Oh, sir," he cried, "can you tell
me where it went to?"
"It came here."
"Here?"
"Yes, and a most remarkable bird it proved. I don't wonder that
you should take an interest in it. It laid an egg after it was
dead--the bonniest, brightest little blue egg that ever was seen.
I have it here in my museum."
Our visitor staggered to his feet and clutched the mantelpiece
with his right hand. Holmes unlocked his strong-box and held up
the blue carbuncle, which shone out like a star, with a cold,
brilliant, many-pointed radiance. Ryder stood glaring with a
drawn face, uncertain whether to claim or to disown it.
"The game's up, Ryder," said Holmes quietly. "Hold up, man, or
you'll be into the fire! Give him an arm back into his chair,
Watson. He's not got blood enough to go in for felony with
impunity. Give him a dash of brandy. So! Now he looks a little
more human. What a shrimp it is, to be sure!"
For a moment he had staggered and nearly fallen, but the brandy
brought a tinge of colour into his cheeks, and he sat staring
with frightened eyes at his accuser.
"I have almost every link in my hands, and all the proofs which I
could possibly need, so there is little which you need tell me.
Still, that little may as well be cleared up to make the case
complete. You had heard, Ryder, of this blue stone of the
Countess of Morcar's?"
"It was Catherine Cusack who told me of it," said he in a
crackling voice.
"I see--her ladyship's waiting-maid. Well, the temptation of
sudden wealth so easily acquired was too much for you, as it has
been for better men before you; but you were not very scrupulous
in the means you used. It seems to me, Ryder, that there is the
making of a very pretty villain in you. You knew that this man
Horner, the plumber, had been concerned in some such matter
before, and that suspicion would rest the more readily upon him.
What did you do, then? You made some small job in my lady's
room--you and your confederate Cusack--and you managed that he
should be the man sent for. Then, when he had left, you rifled
the jewel-case, raised the alarm, and had this unfortunate man
arrested. You then--"
Ryder threw himself down suddenly upon the rug and clutched at my
companion's knees. "For God's sake, have mercy!" he shrieked.
"Think of my father! Of my mother! It would break their hearts. I
never went wrong before! I never will again. I swear it. I'll
swear it on a Bible. Oh, don't bring it into court! For Christ's
sake, don't!"
"Get back into your chair!" said Holmes sternly. "It is very well
to cringe and crawl now, but you thought little enough of this
poor Horner in the dock for a crime of which he knew nothing."
"I will fly, Mr. Holmes. I will leave the country, sir. Then the
charge against him will break down."
"Hum! We will talk about that. And now let us hear a true account
of the next act. How came the stone into the goose, and how came
the goose into the open market? Tell us the truth, for there lies
your only hope of safety."
Ryder passed his tongue over his parched lips. "I will tell you
it just as it happened, sir," said he. "When Horner had been
arrested, it seemed to me that it would be best for me to get
away with the stone at once, for I did not know at what moment
the police might not take it into their heads to search me and my
room. There was no place about the hotel where it would be safe.
I went out, as if on some commission, and I made for my sister's
house. She had married a man named Oakshott, and lived in Brixton
Road, where she fattened fowls for the market. All the way there
every man I met seemed to me to be a policeman or a detective;
and, for all that it was a cold night, the sweat was pouring down
my face before I came to the Brixton Road. My sister asked me
what was the matter, and why I was so pale; but I told her that I
had been upset by the jewel robbery at the hotel. Then I went
into the back yard and smoked a pipe and wondered what it would
be best to do.
"I had a friend once called Maudsley, who went to the bad, and
has just been serving his time in Pentonville. One day he had met
me, and fell into talk about the ways of thieves, and how they
could get rid of what they stole. I knew that he would be true to
me, for I knew one or two things about him; so I made up my mind
to go right on to Kilburn, where he lived, and take him into my
confidence. He would show me how to turn the stone into money.
But how to get to him in safety? I thought of the agonies I had
gone through in coming from the hotel. I might at any moment be
seized and searched, and there would be the stone in my waistcoat
pocket. I was leaning against the wall at the time and looking at
the geese which were waddling about round my feet, and suddenly
an idea came into my head which showed me how I could beat the
best detective that ever lived.
"My sister had told me some weeks before that I might have the
pick of her geese for a Christmas present, and I knew that she
was always as good as her word. I would take my goose now, and in
it I would carry my stone to Kilburn. There was a little shed in
the yard, and behind this I drove one of the birds--a fine big
one, white, with a barred tail. I caught it, and prying its bill
open, I thrust the stone down its throat as far as my finger
could reach. The bird gave a gulp, and I felt the stone pass
along its gullet and down into its crop. But the creature flapped
and struggled, and out came my sister to know what was the
matter. As I turned to speak to her the brute broke loose and
fluttered off among the others.
"'Whatever were you doing with that bird, Jem?' says she.
"'Well,' said I, 'you said you'd give me one for Christmas, and I
was feeling which was the fattest.'
"'Oh,' says she, 'we've set yours aside for you--Jem's bird, we
call it. It's the big white one over yonder. There's twenty-six
of them, which makes one for you, and one for us, and two dozen
for the market.'
"'Thank you, Maggie,' says I; 'but if it is all the same to you,
I'd rather have that one I was handling just now.'
"'The other is a good three pound heavier,' said she, 'and we
fattened it expressly for you.'
"'Never mind. I'll have the other, and I'll take it now,' said I.
"'Oh, just as you like,' said she, a little huffed. 'Which is it
you want, then?'
"'That white one with the barred tail, right in the middle of the
flock.'
"'Oh, very well. Kill it and take it with you.'
"Well, I did what she said, Mr. Holmes, and I carried the bird
all the way to Kilburn. I told my pal what I had done, for he was
a man that it was easy to tell a thing like that to. He laughed
until he choked, and we got a knife and opened the goose. My
heart turned to water, for there was no sign of the stone, and I
knew that some terrible mistake had occurred. I left the bird,
rushed back to my sister's, and hurried into the back yard. There
was not a bird to be seen there.
"'Where are they all, Maggie?' I cried.
"'Gone to the dealer's, Jem.'
"'Which dealer's?'
"'Breckinridge, of Covent Garden.'
"'But was there another with a barred tail?' I asked, 'the same
as the one I chose?'
"'Yes, Jem; there were two barred-tailed ones, and I could never
tell them apart.'
"Well, then, of course I saw it all, and I ran off as hard as my
feet would carry me to this man Breckinridge; but he had sold the
lot at once, and not one word would he tell me as to where they
had gone. You heard him yourselves to-night. Well, he has always
answered me like that. My sister thinks that I am going mad.
Sometimes I think that I am myself. And now--and now I am myself
a branded thief, without ever having touched the wealth for which
I sold my character. God help me! God help me!" He burst into
convulsive sobbing, with his face buried in his hands.
There was a long silence, broken only by his heavy breathing and
by the measured tapping of Sherlock Holmes' finger-tips upon the
edge of the table. Then my friend rose and threw open the door.
"Get out!" said he.
"What, sir! Oh, Heaven bless you!"
"No more words. Get out!"
And no more words were needed. There was a rush, a clatter upon
the stairs, the bang of a door, and the crisp rattle of running
footfalls from the street.
"After all, Watson," said Holmes, reaching up his hand for his
clay pipe, "I am not retained by the police to supply their
deficiencies. If Horner were in danger it would be another thing;
but this fellow will not appear against him, and the case must
collapse. I suppose that I am commuting a felony, but it is just
possible that I am saving a soul. This fellow will not go wrong
again; he is too terribly frightened. Send him to gaol now, and
you make him a gaol-bird for life. Besides, it is the season of
forgiveness. Chance has put in our way a most singular and
whimsical problem, and its solution is its own reward. If you
will have the goodness to touch the bell, Doctor, we will begin
another investigation, in which, also a bird will be the chief
feature."
VIII. THE ADVENTURE OF THE SPECKLED BAND
On glancing over my notes of the seventy odd cases in which I
have during the last eight years studied the methods of my friend
Sherlock Holmes, I find many tragic, some comic, a large number
merely strange, but none commonplace; for, working as he did
rather for the love of his art than for the acquirement of
wealth, he refused to associate himself with any investigation
which did not tend towards the unusual, and even the fantastic.
Of all these varied cases, however, I cannot recall any which
presented more singular features than that which was associated
with the well-known Surrey family of the Roylotts of Stoke Moran.
The events in question occurred in the early days of my
association with Holmes, when we were sharing rooms as bachelors
in Baker Street. It is possible that I might have placed them
upon record before, but a promise of secrecy was made at the
time, from which I have only been freed during the last month by
the untimely death of the lady to whom the pledge was given. It
is perhaps as well that the facts should now come to light, for I
have reasons to know that there are widespread rumours as to the
death of Dr. Grimesby Roylott which tend to make the matter even
more terrible than the truth.
It was early in April in the year '83 that I woke one morning to
find Sherlock Holmes standing, fully dressed, by the side of my
bed. He was a late riser, as a rule, and as the clock on the
mantelpiece showed me that it was only a quarter-past seven, I
blinked up at him in some surprise, and perhaps just a little
resentment, for I was myself regular in my habits.
"Very sorry to knock you up, Watson," said he, "but it's the
common lot this morning. Mrs. Hudson has been knocked up, she
retorted upon me, and I on you."
"What is it, then--a fire?"
"No; a client. It seems that a young lady has arrived in a
considerable state of excitement, who insists upon seeing me. She
is waiting now in the sitting-room. Now, when young ladies wander
about the metropolis at this hour of the morning, and knock
sleepy people up out of their beds, I presume that it is
something very pressing which they have to communicate. Should it
prove to be an interesting case, you would, I am sure, wish to
follow it from the outset. I thought, at any rate, that I should
call you and give you the chance."
"My dear fellow, I would not miss it for anything."
I had no keener pleasure than in following Holmes in his
professional investigations, and in admiring the rapid
deductions, as swift as intuitions, and yet always founded on a
logical basis with which he unravelled the problems which were
submitted to him. I rapidly threw on my clothes and was ready in
a few minutes to accompany my friend down to the sitting-room. A
lady dressed in black and heavily veiled, who had been sitting in
the window, rose as we entered.
"Good-morning, madam," said Holmes cheerily. "My name is Sherlock
Holmes. This is my intimate friend and associate, Dr. Watson,
before whom you can speak as freely as before myself. Ha! I am
glad to see that Mrs. Hudson has had the good sense to light the
fire. Pray draw up to it, and I shall order you a cup of hot
coffee, for I observe that you are shivering."
"It is not cold which makes me shiver," said the woman in a low
voice, changing her seat as requested.
"What, then?"
"It is fear, Mr. Holmes. It is terror." She raised her veil as
she spoke, and we could see that she was indeed in a pitiable
state of agitation, her face all drawn and grey, with restless
frightened eyes, like those of some hunted animal. Her features
and figure were those of a woman of thirty, but her hair was shot
with premature grey, and her expression was weary and haggard.
Sherlock Holmes ran her over with one of his quick,
all-comprehensive glances.
"You must not fear," said he soothingly, bending forward and
patting her forearm. "We shall soon set matters right, I have no
doubt. You have come in by train this morning, I see."
"You know me, then?"
"No, but I observe the second half of a return ticket in the palm
of your left glove. You must have started early, and yet you had
a good drive in a dog-cart, along heavy roads, before you reached
the station."
The lady gave a violent start and stared in bewilderment at my
companion.
"There is no mystery, my dear madam," said he, smiling. "The left
arm of your jacket is spattered with mud in no less than seven
places. The marks are perfectly fresh. There is no vehicle save a
dog-cart which throws up mud in that way, and then only when you
sit on the left-hand side of the driver."
"Whatever your reasons may be, you are perfectly correct," said
she. "I started from home before six, reached Leatherhead at
twenty past, and came in by the first train to Waterloo. Sir, I
can stand this strain no longer; I shall go mad if it continues.
I have no one to turn to--none, save only one, who cares for me,
and he, poor fellow, can be of little aid. I have heard of you,
Mr. Holmes; I have heard of you from Mrs. Farintosh, whom you
helped in the hour of her sore need. It was from her that I had
your address. Oh, sir, do you not think that you could help me,
too, and at least throw a little light through the dense darkness
which surrounds me? At present it is out of my power to reward
you for your services, but in a month or six weeks I shall be
married, with the control of my own income, and then at least you
shall not find me ungrateful."
Holmes turned to his desk and, unlocking it, drew out a small
case-book, which he consulted.
"Farintosh," said he. "Ah yes, I recall the case; it was
concerned with an opal tiara. I think it was before your time,
Watson. I can only say, madam, that I shall be happy to devote
the same care to your case as I did to that of your friend. As to
reward, my profession is its own reward; but you are at liberty
to defray whatever expenses I may be put to, at the time which
suits you best. And now I beg that you will lay before us
everything that may help us in forming an opinion upon the
matter."
"Alas!" replied our visitor, "the very horror of my situation
lies in the fact that my fears are so vague, and my suspicions
depend so entirely upon small points, which might seem trivial to
another, that even he to whom of all others I have a right to
look for help and advice looks upon all that I tell him about it
as the fancies of a nervous woman. He does not say so, but I can
read it from his soothing answers and averted eyes. But I have
heard, Mr. Holmes, that you can see deeply into the manifold
wickedness of the human heart. You may advise me how to walk amid
the dangers which encompass me."
"I am all attention, madam."
"My name is Helen Stoner, and I am living with my stepfather, who
is the last survivor of one of the oldest Saxon families in
England, the Roylotts of Stoke Moran, on the western border of
Surrey."
Holmes nodded his head. "The name is familiar to me," said he.
"The family was at one time among the richest in England, and the
estates extended over the borders into Berkshire in the north,
and Hampshire in the west. In the last century, however, four
successive heirs were of a dissolute and wasteful disposition,
and the family ruin was eventually completed by a gambler in the
days of the Regency. Nothing was left save a few acres of ground,
and the two-hundred-year-old house, which is itself crushed under
a heavy mortgage. The last squire dragged out his existence
there, living the horrible life of an aristocratic pauper; but
his only son, my stepfather, seeing that he must adapt himself to
the new conditions, obtained an advance from a relative, which
enabled him to take a medical degree and went out to Calcutta,
where, by his professional skill and his force of character, he
established a large practice. In a fit of anger, however, caused
by some robberies which had been perpetrated in the house, he
beat his native butler to death and narrowly escaped a capital
sentence. As it was, he suffered a long term of imprisonment and
afterwards returned to England a morose and disappointed man.
"When Dr. Roylott was in India he married my mother, Mrs. Stoner,
the young widow of Major-General Stoner, of the Bengal Artillery.
My sister Julia and I were twins, and we were only two years old
at the time of my mother's re-marriage. She had a considerable
sum of money--not less than 1000 pounds a year--and this she
bequeathed to Dr. Roylott entirely while we resided with him,
with a provision that a certain annual sum should be allowed to
each of us in the event of our marriage. Shortly after our return
to England my mother died--she was killed eight years ago in a
railway accident near Crewe. Dr. Roylott then abandoned his
attempts to establish himself in practice in London and took us
to live with him in the old ancestral house at Stoke Moran. The
money which my mother had left was enough for all our wants, and
there seemed to be no obstacle to our happiness.
"But a terrible change came over our stepfather about this time.
Instead of making friends and exchanging visits with our
neighbours, who had at first been overjoyed to see a Roylott of
Stoke Moran back in the old family seat, he shut himself up in
his house and seldom came out save to indulge in ferocious
quarrels with whoever might cross his path. Violence of temper
approaching to mania has been hereditary in the men of the
family, and in my stepfather's case it had, I believe, been
intensified by his long residence in the tropics. A series of
disgraceful brawls took place, two of which ended in the
police-court, until at last he became the terror of the village,
and the folks would fly at his approach, for he is a man of
immense strength, and absolutely uncontrollable in his anger.
"Last week he hurled the local blacksmith over a parapet into a
stream, and it was only by paying over all the money which I
could gather together that I was able to avert another public
exposure. He had no friends at all save the wandering gipsies,
and he would give these vagabonds leave to encamp upon the few
acres of bramble-covered land which represent the family estate,
and would accept in return the hospitality of their tents,
wandering away with them sometimes for weeks on end. He has a
passion also for Indian animals, which are sent over to him by a
correspondent, and he has at this moment a cheetah and a baboon,
which wander freely over his grounds and are feared by the
villagers almost as much as their master.
"You can imagine from what I say that my poor sister Julia and I
had no great pleasure in our lives. No servant would stay with
us, and for a long time we did all the work of the house. She was
but thirty at the time of her death, and yet her hair had already
begun to whiten, even as mine has."
"Your sister is dead, then?"
"She died just two years ago, and it is of her death that I wish
to speak to you. You can understand that, living the life which I
have described, we were little likely to see anyone of our own
age and position. We had, however, an aunt, my mother's maiden
sister, Miss Honoria Westphail, who lives near Harrow, and we
were occasionally allowed to pay short visits at this lady's
house. Julia went there at Christmas two years ago, and met there
a half-pay major of marines, to whom she became engaged. My
stepfather learned of the engagement when my sister returned and
offered no objection to the marriage; but within a fortnight of
the day which had been fixed for the wedding, the terrible event
occurred which has deprived me of my only companion."
Sherlock Holmes had been leaning back in his chair with his eyes
closed and his head sunk in a cushion, but he half opened his
lids now and glanced across at his visitor.
"Pray be precise as to details," said he.
"It is easy for me to be so, for every event of that dreadful
time is seared into my memory. The manor-house is, as I have
already said, very old, and only one wing is now inhabited. The
bedrooms in this wing are on the ground floor, the sitting-rooms
being in the central block of the buildings. Of these bedrooms
the first is Dr. Roylott's, the second my sister's, and the third
my own. There is no communication between them, but they all open
out into the same corridor. Do I make myself plain?"
"Perfectly so."
"The windows of the three rooms open out upon the lawn. That
fatal night Dr. Roylott had gone to his room early, though we
knew that he had not retired to rest, for my sister was troubled
by the smell of the strong Indian cigars which it was his custom
to smoke. She left her room, therefore, and came into mine, where
she sat for some time, chatting about her approaching wedding. At
eleven o'clock she rose to leave me, but she paused at the door
and looked back.
"'Tell me, Helen,' said she, 'have you ever heard anyone whistle
in the dead of the night?'
"'Never,' said I.
"'I suppose that you could not possibly whistle, yourself, in
your sleep?'
"'Certainly not. But why?'
"'Because during the last few nights I have always, about three
in the morning, heard a low, clear whistle. I am a light sleeper,
and it has awakened me. I cannot tell where it came from--perhaps
from the next room, perhaps from the lawn. I thought that I would
just ask you whether you had heard it.'
"'No, I have not. It must be those wretched gipsies in the
plantation.'
"'Very likely. And yet if it were on the lawn, I wonder that you
did not hear it also.'
"'Ah, but I sleep more heavily than you.'
"'Well, it is of no great consequence, at any rate.' She smiled
back at me, closed my door, and a few moments later I heard her
key turn in the lock."
"Indeed," said Holmes. "Was it your custom always to lock
yourselves in at night?"
"Always."
"And why?"
"I think that I mentioned to you that the doctor kept a cheetah
and a baboon. We had no feeling of security unless our doors were
locked."
"Quite so. Pray proceed with your statement."
"I could not sleep that night. A vague feeling of impending
misfortune impressed me. My sister and I, you will recollect,
were twins, and you know how subtle are the links which bind two
souls which are so closely allied. It was a wild night. The wind
was howling outside, and the rain was beating and splashing
against the windows. Suddenly, amid all the hubbub of the gale,
there burst forth the wild scream of a terrified woman. I knew
that it was my sister's voice. I sprang from my bed, wrapped a
shawl round me, and rushed into the corridor. As I opened my door
I seemed to hear a low whistle, such as my sister described, and
a few moments later a clanging sound, as if a mass of metal had
fallen. As I ran down the passage, my sister's door was unlocked,
and revolved slowly upon its hinges. I stared at it
horror-stricken, not knowing what was about to issue from it. By
the light of the corridor-lamp I saw my sister appear at the
opening, her face blanched with terror, her hands groping for
help, her whole figure swaying to and fro like that of a
drunkard. I ran to her and threw my arms round her, but at that
moment her knees seemed to give way and she fell to the ground.
She writhed as one who is in terrible pain, and her limbs were
dreadfully convulsed. At first I thought that she had not
recognised me, but as I bent over her she suddenly shrieked out
in a voice which I shall never forget, 'Oh, my God! Helen! It was
the band! The speckled band!' There was something else which she
would fain have said, and she stabbed with her finger into the
air in the direction of the doctor's room, but a fresh convulsion
seized her and choked her words. I rushed out, calling loudly for
my stepfather, and I met him hastening from his room in his
dressing-gown. When he reached my sister's side she was
unconscious, and though he poured brandy down her throat and sent
for medical aid from the village, all efforts were in vain, for
she slowly sank and died without having recovered her
consciousness. Such was the dreadful end of my beloved sister."
"One moment," said Holmes, "are you sure about this whistle and
metallic sound? Could you swear to it?"
"That was what the county coroner asked me at the inquiry. It is
my strong impression that I heard it, and yet, among the crash of
the gale and the creaking of an old house, I may possibly have
been deceived."
"Was your sister dressed?"
"No, she was in her night-dress. In her right hand was found the
charred stump of a match, and in her left a match-box."
"Showing that she had struck a light and looked about her when
the alarm took place. That is important. And what conclusions did
the coroner come to?"
"He investigated the case with great care, for Dr. Roylott's
conduct had long been notorious in the county, but he was unable
to find any satisfactory cause of death. My evidence showed that
the door had been fastened upon the inner side, and the windows
were blocked by old-fashioned shutters with broad iron bars,
which were secured every night. The walls were carefully sounded,
and were shown to be quite solid all round, and the flooring was
also thoroughly examined, with the same result. The chimney is
wide, but is barred up by four large staples. It is certain,
therefore, that my sister was quite alone when she met her end.
Besides, there were no marks of any violence upon her."
"How about poison?"
"The doctors examined her for it, but without success."
"What do you think that this unfortunate lady died of, then?"
"It is my belief that she died of pure fear and nervous shock,
though what it was that frightened her I cannot imagine."
"Were there gipsies in the plantation at the time?"
"Yes, there are nearly always some there."
"Ah, and what did you gather from this allusion to a band--a
speckled band?"
"Sometimes I have thought that it was merely the wild talk of
delirium, sometimes that it may have referred to some band of
people, perhaps to these very gipsies in the plantation. I do not
know whether the spotted handkerchiefs which so many of them wear
over their heads might have suggested the strange adjective which
she used."
Holmes shook his head like a man who is far from being satisfied.
"These are very deep waters," said he; "pray go on with your
narrative."
"Two years have passed since then, and my life has been until
lately lonelier than ever. A month ago, however, a dear friend,
whom I have known for many years, has done me the honour to ask
my hand in marriage. His name is Armitage--Percy Armitage--the
second son of Mr. Armitage, of Crane Water, near Reading. My
stepfather has offered no opposition to the match, and we are to
be married in the course of the spring. Two days ago some repairs
were started in the west wing of the building, and my bedroom
wall has been pierced, so that I have had to move into the
chamber in which my sister died, and to sleep in the very bed in
which she slept. Imagine, then, my thrill of terror when last
night, as I lay awake, thinking over her terrible fate, I
suddenly heard in the silence of the night the low whistle which
had been the herald of her own death. I sprang up and lit the
lamp, but nothing was to be seen in the room. I was too shaken to
go to bed again, however, so I dressed, and as soon as it was
daylight I slipped down, got a dog-cart at the Crown Inn, which
is opposite, and drove to Leatherhead, from whence I have come on
this morning with the one object of seeing you and asking your
advice."
"You have done wisely," said my friend. "But have you told me
all?"
"Yes, all."
"Miss Roylott, you have not. You are screening your stepfather."
"Why, what do you mean?"
For answer Holmes pushed back the frill of black lace which
fringed the hand that lay upon our visitor's knee. Five little
livid spots, the marks of four fingers and a thumb, were printed
upon the white wrist.
"You have been cruelly used," said Holmes.
The lady coloured deeply and covered over her injured wrist. "He
is a hard man," she said, "and perhaps he hardly knows his own
strength."
There was a long silence, during which Holmes leaned his chin
upon his hands and stared into the crackling fire.
"This is a very deep business," he said at last. "There are a
thousand details which I should desire to know before I decide
upon our course of action. Yet we have not a moment to lose. If
we were to come to Stoke Moran to-day, would it be possible for
us to see over these rooms without the knowledge of your
stepfather?"
"As it happens, he spoke of coming into town to-day upon some
most important business. It is probable that he will be away all
day, and that there would be nothing to disturb you. We have a
housekeeper now, but she is old and foolish, and I could easily
get her out of the way."
"Excellent. You are not averse to this trip, Watson?"
"By no means."
"Then we shall both come. What are you going to do yourself?"
"I have one or two things which I would wish to do now that I am
in town. But I shall return by the twelve o'clock train, so as to
be there in time for your coming."
"And you may expect us early in the afternoon. I have myself some
small business matters to attend to. Will you not wait and
breakfast?"
"No, I must go. My heart is lightened already since I have
confided my trouble to you. I shall look forward to seeing you
again this afternoon." She dropped her thick black veil over her
face and glided from the room.
"And what do you think of it all, Watson?" asked Sherlock Holmes,
leaning back in his chair.
"It seems to me to be a most dark and sinister business."
"Dark enough and sinister enough."
"Yet if the lady is correct in saying that the flooring and walls
are sound, and that the door, window, and chimney are impassable,
then her sister must have been undoubtedly alone when she met her
mysterious end."
"What becomes, then, of these nocturnal whistles, and what of the
very peculiar words of the dying woman?"
"I cannot think."
"When you combine the ideas of whistles at night, the presence of
a band of gipsies who are on intimate terms with this old doctor,
the fact that we have every reason to believe that the doctor has
an interest in preventing his stepdaughter's marriage, the dying
allusion to a band, and, finally, the fact that Miss Helen Stoner
heard a metallic clang, which might have been caused by one of
those metal bars that secured the shutters falling back into its
place, I think that there is good ground to think that the
mystery may be cleared along those lines."
"But what, then, did the gipsies do?"
"I cannot imagine."
"I see many objections to any such theory."
"And so do I. It is precisely for that reason that we are going
to Stoke Moran this day. I want to see whether the objections are
fatal, or if they may be explained away. But what in the name of
the devil!"
The ejaculation had been drawn from my companion by the fact that
our door had been suddenly dashed open, and that a huge man had
framed himself in the aperture. His costume was a peculiar
mixture of the professional and of the agricultural, having a
black top-hat, a long frock-coat, and a pair of high gaiters,
with a hunting-crop swinging in his hand. So tall was he that his
hat actually brushed the cross bar of the doorway, and his
breadth seemed to span it across from side to side. A large face,
seared with a thousand wrinkles, burned yellow with the sun, and
marked with every evil passion, was turned from one to the other
of us, while his deep-set, bile-shot eyes, and his high, thin,
fleshless nose, gave him somewhat the resemblance to a fierce old
bird of prey.
"Which of you is Holmes?" asked this apparition.
"My name, sir; but you have the advantage of me," said my
companion quietly.
"I am Dr. Grimesby Roylott, of Stoke Moran."
"Indeed, Doctor," said Holmes blandly. "Pray take a seat."
"I will do nothing of the kind. My stepdaughter has been here. I
have traced her. What has she been saying to you?"
"It is a little cold for the time of the year," said Holmes.
"What has she been saying to you?" screamed the old man
furiously.
"But I have heard that the crocuses promise well," continued my
companion imperturbably.
"Ha! You put me off, do you?" said our new visitor, taking a step
forward and shaking his hunting-crop. "I know you, you scoundrel!
I have heard of you before. You are Holmes, the meddler."
My friend smiled.
"Holmes, the busybody!"
His smile broadened.
"Holmes, the Scotland Yard Jack-in-office!"
Holmes chuckled heartily. "Your conversation is most
entertaining," said he. "When you go out close the door, for
there is a decided draught."
"I will go when I have said my say. Don't you dare to meddle with
my affairs. I know that Miss Stoner has been here. I traced her!
I am a dangerous man to fall foul of! See here." He stepped
swiftly forward, seized the poker, and bent it into a curve with
his huge brown hands.
"See that you keep yourself out of my grip," he snarled, and
hurling the twisted poker into the fireplace he strode out of the
room.
"He seems a very amiable person," said Holmes, laughing. "I am
not quite so bulky, but if he had remained I might have shown him
that my grip was not much more feeble than his own." As he spoke
he picked up the steel poker and, with a sudden effort,
straightened it out again.
"Fancy his having the insolence to confound me with the official
detective force! This incident gives zest to our investigation,
however, and I only trust that our little friend will not suffer
from her imprudence in allowing this brute to trace her. And now,
Watson, we shall order breakfast, and afterwards I shall walk
down to Doctors' Commons, where I hope to get some data which may
help us in this matter."
It was nearly one o'clock when Sherlock Holmes returned from his
excursion. He held in his hand a sheet of blue paper, scrawled
over with notes and figures.
"I have seen the will of the deceased wife," said he. "To
determine its exact meaning I have been obliged to work out the
present prices of the investments with which it is concerned. The
total income, which at the time of the wife's death was little
short of 1100 pounds, is now, through the fall in agricultural
prices, not more than 750 pounds. Each daughter can claim an
income of 250 pounds, in case of marriage. It is evident,
therefore, that if both girls had married, this beauty would have
had a mere pittance, while even one of them would cripple him to
a very serious extent. My morning's work has not been wasted,
since it has proved that he has the very strongest motives for
standing in the way of anything of the sort. And now, Watson,
this is too serious for dawdling, especially as the old man is
aware that we are interesting ourselves in his affairs; so if you
are ready, we shall call a cab and drive to Waterloo. I should be
very much obliged if you would slip your revolver into your
pocket. An Eley's No. 2 is an excellent argument with gentlemen
who can twist steel pokers into knots. That and a tooth-brush
are, I think, all that we need."
At Waterloo we were fortunate in catching a train for
Leatherhead, where we hired a trap at the station inn and drove
for four or five miles through the lovely Surrey lanes. It was a
perfect day, with a bright sun and a few fleecy clouds in the
heavens. The trees and wayside hedges were just throwing out
their first green shoots, and the air was full of the pleasant
smell of the moist earth. To me at least there was a strange
contrast between the sweet promise of the spring and this
sinister quest upon which we were engaged. My companion sat in
the front of the trap, his arms folded, his hat pulled down over
his eyes, and his chin sunk upon his breast, buried in the
deepest thought. Suddenly, however, he started, tapped me on the
shoulder, and pointed over the meadows.
"Look there!" said he.
A heavily timbered park stretched up in a gentle slope,
thickening into a grove at the highest point. From amid the
branches there jutted out the grey gables and high roof-tree of a
very old mansion.
"Stoke Moran?" said he.
"Yes, sir, that be the house of Dr. Grimesby Roylott," remarked
the driver.
"There is some building going on there," said Holmes; "that is
where we are going."
"There's the village," said the driver, pointing to a cluster of
roofs some distance to the left; "but if you want to get to the
house, you'll find it shorter to get over this stile, and so by
the foot-path over the fields. There it is, where the lady is
walking."
"And the lady, I fancy, is Miss Stoner," observed Holmes, shading
his eyes. "Yes, I think we had better do as you suggest."
We got off, paid our fare, and the trap rattled back on its way
to Leatherhead.
"I thought it as well," said Holmes as we climbed the stile,
"that this fellow should think we had come here as architects, or
on some definite business. It may stop his gossip.
Good-afternoon, Miss Stoner. You see that we have been as good as
our word."
Our client of the morning had hurried forward to meet us with a
face which spoke her joy. "I have been waiting so eagerly for
you," she cried, shaking hands with us warmly. "All has turned
out splendidly. Dr. Roylott has gone to town, and it is unlikely
that he will be back before evening."
"We have had the pleasure of making the doctor's acquaintance,"
said Holmes, and in a few words he sketched out what had
occurred. Miss Stoner turned white to the lips as she listened.
"Good heavens!" she cried, "he has followed me, then."
"So it appears."
"He is so cunning that I never know when I am safe from him. What
will he say when he returns?"
"He must guard himself, for he may find that there is someone
more cunning than himself upon his track. You must lock yourself
up from him to-night. If he is violent, we shall take you away to
your aunt's at Harrow. Now, we must make the best use of our
time, so kindly take us at once to the rooms which we are to
examine."
The building was of grey, lichen-blotched stone, with a high
central portion and two curving wings, like the claws of a crab,
thrown out on each side. In one of these wings the windows were
broken and blocked with wooden boards, while the roof was partly
caved in, a picture of ruin. The central portion was in little
better repair, but the right-hand block was comparatively modern,
and the blinds in the windows, with the blue smoke curling up
from the chimneys, showed that this was where the family resided.
Some scaffolding had been erected against the end wall, and the
stone-work had been broken into, but there were no signs of any
workmen at the moment of our visit. Holmes walked slowly up and
down the ill-trimmed lawn and examined with deep attention the
outsides of the windows.
"This, I take it, belongs to the room in which you used to sleep,
the centre one to your sister's, and the one next to the main
building to Dr. Roylott's chamber?"
"Exactly so. But I am now sleeping in the middle one."
"Pending the alterations, as I understand. By the way, there does
not seem to be any very pressing need for repairs at that end
wall."
"There were none. I believe that it was an excuse to move me from
my room."
"Ah! that is suggestive. Now, on the other side of this narrow
wing runs the corridor from which these three rooms open. There
are windows in it, of course?"
"Yes, but very small ones. Too narrow for anyone to pass
through."
"As you both locked your doors at night, your rooms were
unapproachable from that side. Now, would you have the kindness
to go into your room and bar your shutters?"
Miss Stoner did so, and Holmes, after a careful examination
through the open window, endeavoured in every way to force the
shutter open, but without success. There was no slit through
which a knife could be passed to raise the bar. Then with his
lens he tested the hinges, but they were of solid iron, built
firmly into the massive masonry. "Hum!" said he, scratching his
chin in some perplexity, "my theory certainly presents some
difficulties. No one could pass these shutters if they were
bolted. Well, we shall see if the inside throws any light upon
the matter."
A small side door led into the whitewashed corridor from which
the three bedrooms opened. Holmes refused to examine the third
chamber, so we passed at once to the second, that in which Miss
Stoner was now sleeping, and in which her sister had met with her
fate. It was a homely little room, with a low ceiling and a
gaping fireplace, after the fashion of old country-houses. A
brown chest of drawers stood in one corner, a narrow
white-counterpaned bed in another, and a dressing-table on the
left-hand side of the window. These articles, with two small
wicker-work chairs, made up all the furniture in the room save
for a square of Wilton carpet in the centre. The boards round and
the panelling of the walls were of brown, worm-eaten oak, so old
and discoloured that it may have dated from the original building
of the house. Holmes drew one of the chairs into a corner and sat
silent, while his eyes travelled round and round and up and down,
taking in every detail of the apartment.
"Where does that bell communicate with?" he asked at last
pointing to a thick bell-rope which hung down beside the bed, the
tassel actually lying upon the pillow.
"It goes to the housekeeper's room."
"It looks newer than the other things?"
"Yes, it was only put there a couple of years ago."
"Your sister asked for it, I suppose?"
"No, I never heard of her using it. We used always to get what we
wanted for ourselves."
"Indeed, it seemed unnecessary to put so nice a bell-pull there.
You will excuse me for a few minutes while I satisfy myself as to
this floor." He threw himself down upon his face with his lens in
his hand and crawled swiftly backward and forward, examining
minutely the cracks between the boards. Then he did the same with
the wood-work with which the chamber was panelled. Finally he
walked over to the bed and spent some time in staring at it and
in running his eye up and down the wall. Finally he took the
bell-rope in his hand and gave it a brisk tug.
"Why, it's a dummy," said he.
"Won't it ring?"
"No, it is not even attached to a wire. This is very interesting.
You can see now that it is fastened to a hook just above where
the little opening for the ventilator is."
"How very absurd! I never noticed that before."
"Very strange!" muttered Holmes, pulling at the rope. "There are
one or two very singular points about this room. For example,
what a fool a builder must be to open a ventilator into another
room, when, with the same trouble, he might have communicated
with the outside air!"
"That is also quite modern," said the lady.
"Done about the same time as the bell-rope?" remarked Holmes.
"Yes, there were several little changes carried out about that
time."
"They seem to have been of a most interesting character--dummy
bell-ropes, and ventilators which do not ventilate. With your
permission, Miss Stoner, we shall now carry our researches into
the inner apartment."
Dr. Grimesby Roylott's chamber was larger than that of his
step-daughter, but was as plainly furnished. A camp-bed, a small
wooden shelf full of books, mostly of a technical character, an
armchair beside the bed, a plain wooden chair against the wall, a
round table, and a large iron safe were the principal things
which met the eye. Holmes walked slowly round and examined each
and all of them with the keenest interest.
"What's in here?" he asked, tapping the safe.
"My stepfather's business papers."
"Oh! you have seen inside, then?"
"Only once, some years ago. I remember that it was full of
papers."
"There isn't a cat in it, for example?"
"No. What a strange idea!"
"Well, look at this!" He took up a small saucer of milk which
stood on the top of it.
"No; we don't keep a cat. But there is a cheetah and a baboon."
"Ah, yes, of course! Well, a cheetah is just a big cat, and yet a
saucer of milk does not go very far in satisfying its wants, I
daresay. There is one point which I should wish to determine." He
squatted down in front of the wooden chair and examined the seat
of it with the greatest attention.
"Thank you. That is quite settled," said he, rising and putting
his lens in his pocket. "Hullo! Here is something interesting!"
The object which had caught his eye was a small dog lash hung on
one corner of the bed. The lash, however, was curled upon itself
and tied so as to make a loop of whipcord.
"What do you make of that, Watson?"
"It's a common enough lash. But I don't know why it should be
tied."
"That is not quite so common, is it? Ah, me! it's a wicked world,
and when a clever man turns his brains to crime it is the worst
of all. I think that I have seen enough now, Miss Stoner, and
with your permission we shall walk out upon the lawn."
I had never seen my friend's face so grim or his brow so dark as
it was when we turned from the scene of this investigation. We
had walked several times up and down the lawn, neither Miss
Stoner nor myself liking to break in upon his thoughts before he
roused himself from his reverie.
"It is very essential, Miss Stoner," said he, "that you should
absolutely follow my advice in every respect."
"I shall most certainly do so."
"The matter is too serious for any hesitation. Your life may
depend upon your compliance."
"I assure you that I am in your hands."
"In the first place, both my friend and I must spend the night in
your room."
Both Miss Stoner and I gazed at him in astonishment.
"Yes, it must be so. Let me explain. I believe that that is the
village inn over there?"
"Yes, that is the Crown."
"Very good. Your windows would be visible from there?"
"Certainly."
"You must confine yourself to your room, on pretence of a
headache, when your stepfather comes back. Then when you hear him
retire for the night, you must open the shutters of your window,
undo the hasp, put your lamp there as a signal to us, and then
withdraw quietly with everything which you are likely to want
into the room which you used to occupy. I have no doubt that, in
spite of the repairs, you could manage there for one night."
"Oh, yes, easily."
"The rest you will leave in our hands."
"But what will you do?"
"We shall spend the night in your room, and we shall investigate
the cause of this noise which has disturbed you."
"I believe, Mr. Holmes, that you have already made up your mind,"
said Miss Stoner, laying her hand upon my companion's sleeve.
"Perhaps I have."
"Then, for pity's sake, tell me what was the cause of my sister's
death."
"I should prefer to have clearer proofs before I speak."
"You can at least tell me whether my own thought is correct, and
if she died from some sudden fright."
"No, I do not think so. I think that there was probably some more
tangible cause. And now, Miss Stoner, we must leave you for if
Dr. Roylott returned and saw us our journey would be in vain.
Good-bye, and be brave, for if you will do what I have told you,
you may rest assured that we shall soon drive away the dangers
that threaten you."
Sherlock Holmes and I had no difficulty in engaging a bedroom and
sitting-room at the Crown Inn. They were on the upper floor, and
from our window we could command a view of the avenue gate, and
of the inhabited wing of Stoke Moran Manor House. At dusk we saw
Dr. Grimesby Roylott drive past, his huge form looming up beside
the little figure of the lad who drove him. The boy had some
slight difficulty in undoing the heavy iron gates, and we heard
the hoarse roar of the doctor's voice and saw the fury with which
he shook his clinched fists at him. The trap drove on, and a few
minutes later we saw a sudden light spring up among the trees as
the lamp was lit in one of the sitting-rooms.
"Do you know, Watson," said Holmes as we sat together in the
gathering darkness, "I have really some scruples as to taking you
to-night. There is a distinct element of danger."
"Can I be of assistance?"
"Your presence might be invaluable."
"Then I shall certainly come."
"It is very kind of you."
"You speak of danger. You have evidently seen more in these rooms
than was visible to me."
"No, but I fancy that I may have deduced a little more. I imagine
that you saw all that I did."
"I saw nothing remarkable save the bell-rope, and what purpose
that could answer I confess is more than I can imagine."
"You saw the ventilator, too?"
"Yes, but I do not think that it is such a very unusual thing to
have a small opening between two rooms. It was so small that a
rat could hardly pass through."
"I knew that we should find a ventilator before ever we came to
Stoke Moran."
"My dear Holmes!"
"Oh, yes, I did. You remember in her statement she said that her
sister could smell Dr. Roylott's cigar. Now, of course that
suggested at once that there must be a communication between the
two rooms. It could only be a small one, or it would have been
remarked upon at the coroner's inquiry. I deduced a ventilator."
"But what harm can there be in that?"
"Well, there is at least a curious coincidence of dates. A
ventilator is made, a cord is hung, and a lady who sleeps in the
bed dies. Does not that strike you?"
"I cannot as yet see any connection."
"Did you observe anything very peculiar about that bed?"
"No."
"It was clamped to the floor. Did you ever see a bed fastened
like that before?"
"I cannot say that I have."
"The lady could not move her bed. It must always be in the same
relative position to the ventilator and to the rope--or so we may
call it, since it was clearly never meant for a bell-pull."
"Holmes," I cried, "I seem to see dimly what you are hinting at.
We are only just in time to prevent some subtle and horrible
crime."
"Subtle enough and horrible enough. When a doctor does go wrong
he is the first of criminals. He has nerve and he has knowledge.
Palmer and Pritchard were among the heads of their profession.
This man strikes even deeper, but I think, Watson, that we shall
be able to strike deeper still. But we shall have horrors enough
before the night is over; for goodness' sake let us have a quiet
pipe and turn our minds for a few hours to something more
cheerful."
About nine o'clock the light among the trees was extinguished,
and all was dark in the direction of the Manor House. Two hours
passed slowly away, and then, suddenly, just at the stroke of
eleven, a single bright light shone out right in front of us.
"That is our signal," said Holmes, springing to his feet; "it
comes from the middle window."
As we passed out he exchanged a few words with the landlord,
explaining that we were going on a late visit to an acquaintance,
and that it was possible that we might spend the night there. A
moment later we were out on the dark road, a chill wind blowing
in our faces, and one yellow light twinkling in front of us
through the gloom to guide us on our sombre errand.
There was little difficulty in entering the grounds, for
unrepaired breaches gaped in the old park wall. Making our way
among the trees, we reached the lawn, crossed it, and were about
to enter through the window when out from a clump of laurel
bushes there darted what seemed to be a hideous and distorted
child, who threw itself upon the grass with writhing limbs and
then ran swiftly across the lawn into the darkness.
"My God!" I whispered; "did you see it?"
Holmes was for the moment as startled as I. His hand closed like
a vice upon my wrist in his agitation. Then he broke into a low
laugh and put his lips to my ear.
"It is a nice household," he murmured. "That is the baboon."
I had forgotten the strange pets which the doctor affected. There
was a cheetah, too; perhaps we might find it upon our shoulders
at any moment. I confess that I felt easier in my mind when,
after following Holmes' example and slipping off my shoes, I
found myself inside the bedroom. My companion noiselessly closed
the shutters, moved the lamp onto the table, and cast his eyes
round the room. All was as we had seen it in the daytime. Then
creeping up to me and making a trumpet of his hand, he whispered
into my ear again so gently that it was all that I could do to
distinguish the words:
"The least sound would be fatal to our plans."
I nodded to show that I had heard.
"We must sit without light. He would see it through the
ventilator."
I nodded again.
"Do not go asleep; your very life may depend upon it. Have your
pistol ready in case we should need it. I will sit on the side of
the bed, and you in that chair."
I took out my revolver and laid it on the corner of the table.
Holmes had brought up a long thin cane, and this he placed upon
the bed beside him. By it he laid the box of matches and the
stump of a candle. Then he turned down the lamp, and we were left
in darkness.
How shall I ever forget that dreadful vigil? I could not hear a
sound, not even the drawing of a breath, and yet I knew that my
companion sat open-eyed, within a few feet of me, in the same
state of nervous tension in which I was myself. The shutters cut
off the least ray of light, and we waited in absolute darkness.
From outside came the occasional cry of a night-bird, and once at
our very window a long drawn catlike whine, which told us that
the cheetah was indeed at liberty. Far away we could hear the
deep tones of the parish clock, which boomed out every quarter of
an hour. How long they seemed, those quarters! Twelve struck, and
one and two and three, and still we sat waiting silently for
whatever might befall.
Suddenly there was the momentary gleam of a light up in the
direction of the ventilator, which vanished immediately, but was
succeeded by a strong smell of burning oil and heated metal.
Someone in the next room had lit a dark-lantern. I heard a gentle
sound of movement, and then all was silent once more, though the
smell grew stronger. For half an hour I sat with straining ears.
Then suddenly another sound became audible--a very gentle,
soothing sound, like that of a small jet of steam escaping
continually from a kettle. The instant that we heard it, Holmes
sprang from the bed, struck a match, and lashed furiously with
his cane at the bell-pull.
"You see it, Watson?" he yelled. "You see it?"
But I saw nothing. At the moment when Holmes struck the light I
heard a low, clear whistle, but the sudden glare flashing into my
weary eyes made it impossible for me to tell what it was at which
my friend lashed so savagely. I could, however, see that his face
was deadly pale and filled with horror and loathing. He had
ceased to strike and was gazing up at the ventilator when
suddenly there broke from the silence of the night the most
horrible cry to which I have ever listened. It swelled up louder
and louder, a hoarse yell of pain and fear and anger all mingled
in the one dreadful shriek. They say that away down in the
village, and even in the distant parsonage, that cry raised the
sleepers from their beds. It struck cold to our hearts, and I
stood gazing at Holmes, and he at me, until the last echoes of it
had died away into the silence from which it rose.
"What can it mean?" I gasped.
"It means that it is all over," Holmes answered. "And perhaps,
after all, it is for the best. Take your pistol, and we will
enter Dr. Roylott's room."
With a grave face he lit the lamp and led the way down the
corridor. Twice he struck at the chamber door without any reply
from within. Then he turned the handle and entered, I at his
heels, with the cocked pistol in my hand.
It was a singular sight which met our eyes. On the table stood a
dark-lantern with the shutter half open, throwing a brilliant
beam of light upon the iron safe, the door of which was ajar.
Beside this table, on the wooden chair, sat Dr. Grimesby Roylott
clad in a long grey dressing-gown, his bare ankles protruding
beneath, and his feet thrust into red heelless Turkish slippers.
Across his lap lay the short stock with the long lash which we
had noticed during the day. His chin was cocked upward and his
eyes were fixed in a dreadful, rigid stare at the corner of the
ceiling. Round his brow he had a peculiar yellow band, with
brownish speckles, which seemed to be bound tightly round his
head. As we entered he made neither sound nor motion.
"The band! the speckled band!" whispered Holmes.
I took a step forward. In an instant his strange headgear began
to move, and there reared itself from among his hair the squat
diamond-shaped head and puffed neck of a loathsome serpent.
"It is a swamp adder!" cried Holmes; "the deadliest snake in
India. He has died within ten seconds of being bitten. Violence
does, in truth, recoil upon the violent, and the schemer falls
into the pit which he digs for another. Let us thrust this
creature back into its den, and we can then remove Miss Stoner to
some place of shelter and let the county police know what has
happened."
As he spoke he drew the dog-whip swiftly from the dead man's lap,
and throwing the noose round the reptile's neck he drew it from
its horrid perch and, carrying it at arm's length, threw it into
the iron safe, which he closed upon it.
Such are the true facts of the death of Dr. Grimesby Roylott, of
Stoke Moran. It is not necessary that I should prolong a
narrative which has already run to too great a length by telling
how we broke the sad news to the terrified girl, how we conveyed
her by the morning train to the care of her good aunt at Harrow,
of how the slow process of official inquiry came to the
conclusion that the doctor met his fate while indiscreetly
playing with a dangerous pet. The little which I had yet to learn
of the case was told me by Sherlock Holmes as we travelled back
next day.
"I had," said he, "come to an entirely erroneous conclusion which
shows, my dear Watson, how dangerous it always is to reason from
insufficient data. The presence of the gipsies, and the use of
the word 'band,' which was used by the poor girl, no doubt, to
explain the appearance which she had caught a hurried glimpse of
by the light of her match, were sufficient to put me upon an
entirely wrong scent. I can only claim the merit that I instantly
reconsidered my position when, however, it became clear to me
that whatever danger threatened an occupant of the room could not
come either from the window or the door. My attention was
speedily drawn, as I have already remarked to you, to this
ventilator, and to the bell-rope which hung down to the bed. The
discovery that this was a dummy, and that the bed was clamped to
the floor, instantly gave rise to the suspicion that the rope was
there as a bridge for something passing through the hole and
coming to the bed. The idea of a snake instantly occurred to me,
and when I coupled it with my knowledge that the doctor was
furnished with a supply of creatures from India, I felt that I
was probably on the right track. The idea of using a form of
poison which could not possibly be discovered by any chemical
test was just such a one as would occur to a clever and ruthless
man who had had an Eastern training. The rapidity with which such
a poison would take effect would also, from his point of view, be
an advantage. It would be a sharp-eyed coroner, indeed, who could
distinguish the two little dark punctures which would show where
the poison fangs had done their work. Then I thought of the
whistle. Of course he must recall the snake before the morning
light revealed it to the victim. He had trained it, probably by
the use of the milk which we saw, to return to him when summoned.
He would put it through this ventilator at the hour that he
thought best, with the certainty that it would crawl down the
rope and land on the bed. It might or might not bite the
occupant, perhaps she might escape every night for a week, but
sooner or later she must fall a victim.
"I had come to these conclusions before ever I had entered his
room. An inspection of his chair showed me that he had been in
the habit of standing on it, which of course would be necessary
in order that he should reach the ventilator. The sight of the
safe, the saucer of milk, and the loop of whipcord were enough to
finally dispel any doubts which may have remained. The metallic
clang heard by Miss Stoner was obviously caused by her stepfather
hastily closing the door of his safe upon its terrible occupant.
Having once made up my mind, you know the steps which I took in
order to put the matter to the proof. I heard the creature hiss
as I have no doubt that you did also, and I instantly lit the
light and attacked it."
"With the result of driving it through the ventilator."
"And also with the result of causing it to turn upon its master
at the other side. Some of the blows of my cane came home and
roused its snakish temper, so that it flew upon the first person
it saw. In this way I am no doubt indirectly responsible for Dr.
Grimesby Roylott's death, and I cannot say that it is likely to
weigh very heavily upon my conscience."
IX. THE ADVENTURE OF THE ENGINEER'S THUMB
Of all the problems which have been submitted to my friend, Mr.
Sherlock Holmes, for solution during the years of our intimacy,
there were only two which I was the means of introducing to his
notice--that of Mr. Hatherley's thumb, and that of Colonel
Warburton's madness. Of these the latter may have afforded a
finer field for an acute and original observer, but the other was
so strange in its inception and so dramatic in its details that
it may be the more worthy of being placed upon record, even if it
gave my friend fewer openings for those deductive methods of
reasoning by which he achieved such remarkable results. The story
has, I believe, been told more than once in the newspapers, but,
like all such narratives, its effect is much less striking when
set forth en bloc in a single half-column of print than when the
facts slowly evolve before your own eyes, and the mystery clears
gradually away as each new discovery furnishes a step which leads
on to the complete truth. At the time the circumstances made a
deep impression upon me, and the lapse of two years has hardly
served to weaken the effect.
It was in the summer of '89, not long after my marriage, that the
events occurred which I am now about to summarise. I had returned
to civil practice and had finally abandoned Holmes in his Baker
Street rooms, although I continually visited him and occasionally
even persuaded him to forgo his Bohemian habits so far as to come
and visit us. My practice had steadily increased, and as I
happened to live at no very great distance from Paddington
Station, I got a few patients from among the officials. One of
these, whom I had cured of a painful and lingering disease, was
never weary of advertising my virtues and of endeavouring to send
me on every sufferer over whom he might have any influence.
One morning, at a little before seven o'clock, I was awakened by
the maid tapping at the door to announce that two men had come
from Paddington and were waiting in the consulting-room. I
dressed hurriedly, for I knew by experience that railway cases
were seldom trivial, and hastened downstairs. As I descended, my
old ally, the guard, came out of the room and closed the door
tightly behind him.
"I've got him here," he whispered, jerking his thumb over his
shoulder; "he's all right."
"What is it, then?" I asked, for his manner suggested that it was
some strange creature which he had caged up in my room.
"It's a new patient," he whispered. "I thought I'd bring him
round myself; then he couldn't slip away. There he is, all safe
and sound. I must go now, Doctor; I have my dooties, just the
same as you." And off he went, this trusty tout, without even
giving me time to thank him.
I entered my consulting-room and found a gentleman seated by the
table. He was quietly dressed in a suit of heather tweed with a
soft cloth cap which he had laid down upon my books. Round one of
his hands he had a handkerchief wrapped, which was mottled all
over with bloodstains. He was young, not more than
five-and-twenty, I should say, with a strong, masculine face; but
he was exceedingly pale and gave me the impression of a man who
was suffering from some strong agitation, which it took all his
strength of mind to control.
"I am sorry to knock you up so early, Doctor," said he, "but I
have had a very serious accident during the night. I came in by
train this morning, and on inquiring at Paddington as to where I
might find a doctor, a worthy fellow very kindly escorted me
here. I gave the maid a card, but I see that she has left it upon
the side-table."
I took it up and glanced at it. "Mr. Victor Hatherley, hydraulic
engineer, 16A, Victoria Street (3rd floor)." That was the name,
style, and abode of my morning visitor. "I regret that I have
kept you waiting," said I, sitting down in my library-chair. "You
are fresh from a night journey, I understand, which is in itself
a monotonous occupation."
"Oh, my night could not be called monotonous," said he, and
laughed. He laughed very heartily, with a high, ringing note,
leaning back in his chair and shaking his sides. All my medical
instincts rose up against that laugh.
"Stop it!" I cried; "pull yourself together!" and I poured out
some water from a caraffe.
It was useless, however. He was off in one of those hysterical
outbursts which come upon a strong nature when some great crisis
is over and gone. Presently he came to himself once more, very
weary and pale-looking.
"I have been making a fool of myself," he gasped.
"Not at all. Drink this." I dashed some brandy into the water,
and the colour began to come back to his bloodless cheeks.
"That's better!" said he. "And now, Doctor, perhaps you would
kindly attend to my thumb, or rather to the place where my thumb
used to be."
He unwound the handkerchief and held out his hand. It gave even
my hardened nerves a shudder to look at it. There were four
protruding fingers and a horrid red, spongy surface where the
thumb should have been. It had been hacked or torn right out from
the roots.
"Good heavens!" I cried, "this is a terrible injury. It must have
bled considerably."
"Yes, it did. I fainted when it was done, and I think that I must
have been senseless for a long time. When I came to I found that
it was still bleeding, so I tied one end of my handkerchief very
tightly round the wrist and braced it up with a twig."
"Excellent! You should have been a surgeon."
"It is a question of hydraulics, you see, and came within my own
province."
"This has been done," said I, examining the wound, "by a very
heavy and sharp instrument."
"A thing like a cleaver," said he.
"An accident, I presume?"
"By no means."
"What! a murderous attack?"
"Very murderous indeed."
"You horrify me."
I sponged the wound, cleaned it, dressed it, and finally covered
it over with cotton wadding and carbolised bandages. He lay back
without wincing, though he bit his lip from time to time.
"How is that?" I asked when I had finished.
"Capital! Between your brandy and your bandage, I feel a new man.
I was very weak, but I have had a good deal to go through."
"Perhaps you had better not speak of the matter. It is evidently
trying to your nerves."
"Oh, no, not now. I shall have to tell my tale to the police;
but, between ourselves, if it were not for the convincing
evidence of this wound of mine, I should be surprised if they
believed my statement, for it is a very extraordinary one, and I
have not much in the way of proof with which to back it up; and,
even if they believe me, the clues which I can give them are so
vague that it is a question whether justice will be done."
"Ha!" cried I, "if it is anything in the nature of a problem
which you desire to see solved, I should strongly recommend you
to come to my friend, Mr. Sherlock Holmes, before you go to the
official police."
"Oh, I have heard of that fellow," answered my visitor, "and I
should be very glad if he would take the matter up, though of
course I must use the official police as well. Would you give me
an introduction to him?"
"I'll do better. I'll take you round to him myself."
"I should be immensely obliged to you."
"We'll call a cab and go together. We shall just be in time to
have a little breakfast with him. Do you feel equal to it?"
"Yes; I shall not feel easy until I have told my story."
"Then my servant will call a cab, and I shall be with you in an
instant." I rushed upstairs, explained the matter shortly to my
wife, and in five minutes was inside a hansom, driving with my
new acquaintance to Baker Street.
Sherlock Holmes was, as I expected, lounging about his
sitting-room in his dressing-gown, reading the agony column of The
Times and smoking his before-breakfast pipe, which was composed
of all the plugs and dottles left from his smokes of the day
before, all carefully dried and collected on the corner of the
mantelpiece. He received us in his quietly genial fashion,
ordered fresh rashers and eggs, and joined us in a hearty meal.
When it was concluded he settled our new acquaintance upon the
sofa, placed a pillow beneath his head, and laid a glass of
brandy and water within his reach.
"It is easy to see that your experience has been no common one,
Mr. Hatherley," said he. "Pray, lie down there and make yourself
absolutely at home. Tell us what you can, but stop when you are
tired and keep up your strength with a little stimulant."
"Thank you," said my patient, "but I have felt another man since
the doctor bandaged me, and I think that your breakfast has
completed the cure. I shall take up as little of your valuable
time as possible, so I shall start at once upon my peculiar
experiences."
Holmes sat in his big armchair with the weary, heavy-lidded
expression which veiled his keen and eager nature, while I sat
opposite to him, and we listened in silence to the strange story
which our visitor detailed to us.
"You must know," said he, "that I am an orphan and a bachelor,
residing alone in lodgings in London. By profession I am a
hydraulic engineer, and I have had considerable experience of my
work during the seven years that I was apprenticed to Venner &
Matheson, the well-known firm, of Greenwich. Two years ago,
having served my time, and having also come into a fair sum of
money through my poor father's death, I determined to start in
business for myself and took professional chambers in Victoria
Street.
"I suppose that everyone finds his first independent start in
business a dreary experience. To me it has been exceptionally so.
During two years I have had three consultations and one small
job, and that is absolutely all that my profession has brought
me. My gross takings amount to 27 pounds 10s. Every day, from
nine in the morning until four in the afternoon, I waited in my
little den, until at last my heart began to sink, and I came to
believe that I should never have any practice at all.
"Yesterday, however, just as I was thinking of leaving the
office, my clerk entered to say there was a gentleman waiting who
wished to see me upon business. He brought up a card, too, with
the name of 'Colonel Lysander Stark' engraved upon it. Close at
his heels came the colonel himself, a man rather over the middle
size, but of an exceeding thinness. I do not think that I have
ever seen so thin a man. His whole face sharpened away into nose
and chin, and the skin of his cheeks was drawn quite tense over
his outstanding bones. Yet this emaciation seemed to be his
natural habit, and due to no disease, for his eye was bright, his
step brisk, and his bearing assured. He was plainly but neatly
dressed, and his age, I should judge, would be nearer forty than
thirty.
"'Mr. Hatherley?' said he, with something of a German accent.
'You have been recommended to me, Mr. Hatherley, as being a man
who is not only proficient in his profession but is also discreet
and capable of preserving a secret.'
"I bowed, feeling as flattered as any young man would at such an
address. 'May I ask who it was who gave me so good a character?'
"'Well, perhaps it is better that I should not tell you that just
at this moment. I have it from the same source that you are both
an orphan and a bachelor and are residing alone in London.'
"'That is quite correct,' I answered; 'but you will excuse me if
I say that I cannot see how all this bears upon my professional
qualifications. I understand that it was on a professional matter
that you wished to speak to me?'
"'Undoubtedly so. But you will find that all I say is really to
the point. I have a professional commission for you, but absolute
secrecy is quite essential--absolute secrecy, you understand, and
of course we may expect that more from a man who is alone than
from one who lives in the bosom of his family.'
"'If I promise to keep a secret,' said I, 'you may absolutely
depend upon my doing so.'
"He looked very hard at me as I spoke, and it seemed to me that I
had never seen so suspicious and questioning an eye.
"'Do you promise, then?' said he at last.
"'Yes, I promise.'
"'Absolute and complete silence before, during, and after? No
reference to the matter at all, either in word or writing?'
"'I have already given you my word.'
"'Very good.' He suddenly sprang up, and darting like lightning
across the room he flung open the door. The passage outside was
empty.
"'That's all right,' said he, coming back. 'I know that clerks are
sometimes curious as to their master's affairs. Now we can talk
in safety.' He drew up his chair very close to mine and began to
stare at me again with the same questioning and thoughtful look.
"A feeling of repulsion, and of something akin to fear had begun
to rise within me at the strange antics of this fleshless man.
Even my dread of losing a client could not restrain me from
showing my impatience.
"'I beg that you will state your business, sir,' said I; 'my time
is of value.' Heaven forgive me for that last sentence, but the
words came to my lips.
"'How would fifty guineas for a night's work suit you?' he asked.
"'Most admirably.'
"'I say a night's work, but an hour's would be nearer the mark. I
simply want your opinion about a hydraulic stamping machine which
has got out of gear. If you show us what is wrong we shall soon
set it right ourselves. What do you think of such a commission as
that?'
"'The work appears to be light and the pay munificent.'
"'Precisely so. We shall want you to come to-night by the last
train.'
"'Where to?'
"'To Eyford, in Berkshire. It is a little place near the borders
of Oxfordshire, and within seven miles of Reading. There is a
train from Paddington which would bring you there at about
11:15.'
"'Very good.'
"'I shall come down in a carriage to meet you.'
"'There is a drive, then?'
"'Yes, our little place is quite out in the country. It is a good
seven miles from Eyford Station.'
"'Then we can hardly get there before midnight. I suppose there
would be no chance of a train back. I should be compelled to stop
the night.'
"'Yes, we could easily give you a shake-down.'
"'That is very awkward. Could I not come at some more convenient
hour?'
"'We have judged it best that you should come late. It is to
recompense you for any inconvenience that we are paying to you, a
young and unknown man, a fee which would buy an opinion from the
very heads of your profession. Still, of course, if you would
like to draw out of the business, there is plenty of time to do
so.'
"I thought of the fifty guineas, and of how very useful they
would be to me. 'Not at all,' said I, 'I shall be very happy to
accommodate myself to your wishes. I should like, however, to
understand a little more clearly what it is that you wish me to
do.'
"'Quite so. It is very natural that the pledge of secrecy which
we have exacted from you should have aroused your curiosity. I
have no wish to commit you to anything without your having it all
laid before you. I suppose that we are absolutely safe from
eavesdroppers?'
"'Entirely.'
"'Then the matter stands thus. You are probably aware that
fuller's-earth is a valuable product, and that it is only found
in one or two places in England?'
"'I have heard so.'
"'Some little time ago I bought a small place--a very small
place--within ten miles of Reading. I was fortunate enough to
discover that there was a deposit of fuller's-earth in one of my
fields. On examining it, however, I found that this deposit was a
comparatively small one, and that it formed a link between two
very much larger ones upon the right and left--both of them,
however, in the grounds of my neighbours. These good people were
absolutely ignorant that their land contained that which was
quite as valuable as a gold-mine. Naturally, it was to my
interest to buy their land before they discovered its true value,
but unfortunately I had no capital by which I could do this. I
took a few of my friends into the secret, however, and they
suggested that we should quietly and secretly work our own little
deposit and that in this way we should earn the money which would
enable us to buy the neighbouring fields. This we have now been
doing for some time, and in order to help us in our operations we
erected a hydraulic press. This press, as I have already
explained, has got out of order, and we wish your advice upon the
subject. We guard our secret very jealously, however, and if it
once became known that we had hydraulic engineers coming to our
little house, it would soon rouse inquiry, and then, if the facts
came out, it would be good-bye to any chance of getting these
fields and carrying out our plans. That is why I have made you
promise me that you will not tell a human being that you are
going to Eyford to-night. I hope that I make it all plain?'
"'I quite follow you,' said I. 'The only point which I could not
quite understand was what use you could make of a hydraulic press
in excavating fuller's-earth, which, as I understand, is dug out
like gravel from a pit.'
"'Ah!' said he carelessly, 'we have our own process. We compress
the earth into bricks, so as to remove them without revealing
what they are. But that is a mere detail. I have taken you fully
into my confidence now, Mr. Hatherley, and I have shown you how I
trust you.' He rose as he spoke. 'I shall expect you, then, at
Eyford at 11:15.'
"'I shall certainly be there.'
"'And not a word to a soul.' He looked at me with a last long,
questioning gaze, and then, pressing my hand in a cold, dank
grasp, he hurried from the room.
"Well, when I came to think it all over in cool blood I was very
much astonished, as you may both think, at this sudden commission
which had been intrusted to me. On the one hand, of course, I was
glad, for the fee was at least tenfold what I should have asked
had I set a price upon my own services, and it was possible that
this order might lead to other ones. On the other hand, the face
and manner of my patron had made an unpleasant impression upon
me, and I could not think that his explanation of the
fuller's-earth was sufficient to explain the necessity for my
coming at midnight, and his extreme anxiety lest I should tell
anyone of my errand. However, I threw all fears to the winds, ate
a hearty supper, drove to Paddington, and started off, having
obeyed to the letter the injunction as to holding my tongue.
"At Reading I had to change not only my carriage but my station.
However, I was in time for the last train to Eyford, and I
reached the little dim-lit station after eleven o'clock. I was the
only passenger who got out there, and there was no one upon the
platform save a single sleepy porter with a lantern. As I passed
out through the wicket gate, however, I found my acquaintance of
the morning waiting in the shadow upon the other side. Without a
word he grasped my arm and hurried me into a carriage, the door
of which was standing open. He drew up the windows on either
side, tapped on the wood-work, and away we went as fast as the
horse could go."
"One horse?" interjected Holmes.
"Yes, only one."
"Did you observe the colour?"
"Yes, I saw it by the side-lights when I was stepping into the
carriage. It was a chestnut."
"Tired-looking or fresh?"
"Oh, fresh and glossy."
"Thank you. I am sorry to have interrupted you. Pray continue
your most interesting statement."
"Away we went then, and we drove for at least an hour. Colonel
Lysander Stark had said that it was only seven miles, but I
should think, from the rate that we seemed to go, and from the
time that we took, that it must have been nearer twelve. He sat
at my side in silence all the time, and I was aware, more than
once when I glanced in his direction, that he was looking at me
with great intensity. The country roads seem to be not very good
in that part of the world, for we lurched and jolted terribly. I
tried to look out of the windows to see something of where we
were, but they were made of frosted glass, and I could make out
nothing save the occasional bright blur of a passing light. Now
and then I hazarded some remark to break the monotony of the
journey, but the colonel answered only in monosyllables, and the
conversation soon flagged. At last, however, the bumping of the
road was exchanged for the crisp smoothness of a gravel-drive,
and the carriage came to a stand. Colonel Lysander Stark sprang
out, and, as I followed after him, pulled me swiftly into a porch
which gaped in front of us. We stepped, as it were, right out of
the carriage and into the hall, so that I failed to catch the
most fleeting glance of the front of the house. The instant that
I had crossed the threshold the door slammed heavily behind us,
and I heard faintly the rattle of the wheels as the carriage
drove away.
"It was pitch dark inside the house, and the colonel fumbled
about looking for matches and muttering under his breath.
Suddenly a door opened at the other end of the passage, and a
long, golden bar of light shot out in our direction. It grew
broader, and a woman appeared with a lamp in her hand, which she
held above her head, pushing her face forward and peering at us.
I could see that she was pretty, and from the gloss with which
the light shone upon her dark dress I knew that it was a rich
material. She spoke a few words in a foreign tongue in a tone as
though asking a question, and when my companion answered in a
gruff monosyllable she gave such a start that the lamp nearly
fell from her hand. Colonel Stark went up to her, whispered
something in her ear, and then, pushing her back into the room
from whence she had come, he walked towards me again with the
lamp in his hand.
"'Perhaps you will have the kindness to wait in this room for a
few minutes,' said he, throwing open another door. It was a
quiet, little, plainly furnished room, with a round table in the
centre, on which several German books were scattered. Colonel
Stark laid down the lamp on the top of a harmonium beside the
door. 'I shall not keep you waiting an instant,' said he, and
vanished into the darkness.
"I glanced at the books upon the table, and in spite of my
ignorance of German I could see that two of them were treatises
on science, the others being volumes of poetry. Then I walked
across to the window, hoping that I might catch some glimpse of
the country-side, but an oak shutter, heavily barred, was folded
across it. It was a wonderfully silent house. There was an old
clock ticking loudly somewhere in the passage, but otherwise
everything was deadly still. A vague feeling of uneasiness began
to steal over me. Who were these German people, and what were
they doing living in this strange, out-of-the-way place? And
where was the place? I was ten miles or so from Eyford, that was
all I knew, but whether north, south, east, or west I had no
idea. For that matter, Reading, and possibly other large towns,
were within that radius, so the place might not be so secluded,
after all. Yet it was quite certain, from the absolute stillness,
that we were in the country. I paced up and down the room,
humming a tune under my breath to keep up my spirits and feeling
that I was thoroughly earning my fifty-guinea fee.
"Suddenly, without any preliminary sound in the midst of the
utter stillness, the door of my room swung slowly open. The woman
was standing in the aperture, the darkness of the hall behind
her, the yellow light from my lamp beating upon her eager and
beautiful face. I could see at a glance that she was sick with
fear, and the sight sent a chill to my own heart. She held up one
shaking finger to warn me to be silent, and she shot a few
whispered words of broken English at me, her eyes glancing back,
like those of a frightened horse, into the gloom behind her.
"'I would go,' said she, trying hard, as it seemed to me, to
speak calmly; 'I would go. I should not stay here. There is no
good for you to do.'
"'But, madam,' said I, 'I have not yet done what I came for. I
cannot possibly leave until I have seen the machine.'
"'It is not worth your while to wait,' she went on. 'You can pass
through the door; no one hinders.' And then, seeing that I smiled
and shook my head, she suddenly threw aside her constraint and
made a step forward, with her hands wrung together. 'For the love
of Heaven!' she whispered, 'get away from here before it is too
late!'
"But I am somewhat headstrong by nature, and the more ready to
engage in an affair when there is some obstacle in the way. I
thought of my fifty-guinea fee, of my wearisome journey, and of
the unpleasant night which seemed to be before me. Was it all to
go for nothing? Why should I slink away without having carried
out my commission, and without the payment which was my due? This
woman might, for all I knew, be a monomaniac. With a stout
bearing, therefore, though her manner had shaken me more than I
cared to confess, I still shook my head and declared my intention
of remaining where I was. She was about to renew her entreaties
when a door slammed overhead, and the sound of several footsteps
was heard upon the stairs. She listened for an instant, threw up
her hands with a despairing gesture, and vanished as suddenly and
as noiselessly as she had come.
"The newcomers were Colonel Lysander Stark and a short thick man
with a chinchilla beard growing out of the creases of his double
chin, who was introduced to me as Mr. Ferguson.
"'This is my secretary and manager,' said the colonel. 'By the
way, I was under the impression that I left this door shut just
now. I fear that you have felt the draught.'
"'On the contrary,' said I, 'I opened the door myself because I
felt the room to be a little close.'
"He shot one of his suspicious looks at me. 'Perhaps we had
better proceed to business, then,' said he. 'Mr. Ferguson and I
will take you up to see the machine.'
"'I had better put my hat on, I suppose.'
"'Oh, no, it is in the house.'
"'What, you dig fuller's-earth in the house?'
"'No, no. This is only where we compress it. But never mind that.
All we wish you to do is to examine the machine and to let us
know what is wrong with it.'
"We went upstairs together, the colonel first with the lamp, the
fat manager and I behind him. It was a labyrinth of an old house,
with corridors, passages, narrow winding staircases, and little
low doors, the thresholds of which were hollowed out by the
generations who had crossed them. There were no carpets and no
signs of any furniture above the ground floor, while the plaster
was peeling off the walls, and the damp was breaking through in
green, unhealthy blotches. I tried to put on as unconcerned an
air as possible, but I had not forgotten the warnings of the
lady, even though I disregarded them, and I kept a keen eye upon
my two companions. Ferguson appeared to be a morose and silent
man, but I could see from the little that he said that he was at
least a fellow-countryman.
"Colonel Lysander Stark stopped at last before a low door, which
he unlocked. Within was a small, square room, in which the three
of us could hardly get at one time. Ferguson remained outside,
and the colonel ushered me in.
"'We are now,' said he, 'actually within the hydraulic press, and
it would be a particularly unpleasant thing for us if anyone were
to turn it on. The ceiling of this small chamber is really the
end of the descending piston, and it comes down with the force of
many tons upon this metal floor. There are small lateral columns
of water outside which receive the force, and which transmit and
multiply it in the manner which is familiar to you. The machine
goes readily enough, but there is some stiffness in the working
of it, and it has lost a little of its force. Perhaps you will
have the goodness to look it over and to show us how we can set
it right.'
"I took the lamp from him, and I examined the machine very
thoroughly. It was indeed a gigantic one, and capable of
exercising enormous pressure. When I passed outside, however, and
pressed down the levers which controlled it, I knew at once by
the whishing sound that there was a slight leakage, which allowed
a regurgitation of water through one of the side cylinders. An
examination showed that one of the india-rubber bands which was
round the head of a driving-rod had shrunk so as not quite to
fill the socket along which it worked. This was clearly the cause
of the loss of power, and I pointed it out to my companions, who
followed my remarks very carefully and asked several practical
questions as to how they should proceed to set it right. When I
had made it clear to them, I returned to the main chamber of the
machine and took a good look at it to satisfy my own curiosity.
It was obvious at a glance that the story of the fuller's-earth
was the merest fabrication, for it would be absurd to suppose
that so powerful an engine could be designed for so inadequate a
purpose. The walls were of wood, but the floor consisted of a
large iron trough, and when I came to examine it I could see a
crust of metallic deposit all over it. I had stooped and was
scraping at this to see exactly what it was when I heard a
muttered exclamation in German and saw the cadaverous face of the
colonel looking down at me.
"'What are you doing there?' he asked.
"I felt angry at having been tricked by so elaborate a story as
that which he had told me. 'I was admiring your fuller's-earth,'
said I; 'I think that I should be better able to advise you as to
your machine if I knew what the exact purpose was for which it
was used.'
"The instant that I uttered the words I regretted the rashness of
my speech. His face set hard, and a baleful light sprang up in
his grey eyes.
"'Very well,' said he, 'you shall know all about the machine.' He
took a step backward, slammed the little door, and turned the key
in the lock. I rushed towards it and pulled at the handle, but it
was quite secure, and did not give in the least to my kicks and
shoves. 'Hullo!' I yelled. 'Hullo! Colonel! Let me out!'
"And then suddenly in the silence I heard a sound which sent my
heart into my mouth. It was the clank of the levers and the swish
of the leaking cylinder. He had set the engine at work. The lamp
still stood upon the floor where I had placed it when examining
the trough. By its light I saw that the black ceiling was coming
down upon me, slowly, jerkily, but, as none knew better than
myself, with a force which must within a minute grind me to a
shapeless pulp. I threw myself, screaming, against the door, and
dragged with my nails at the lock. I implored the colonel to let
me out, but the remorseless clanking of the levers drowned my
cries. The ceiling was only a foot or two above my head, and with
my hand upraised I could feel its hard, rough surface. Then it
flashed through my mind that the pain of my death would depend
very much upon the position in which I met it. If I lay on my
face the weight would come upon my spine, and I shuddered to
think of that dreadful snap. Easier the other way, perhaps; and
yet, had I the nerve to lie and look up at that deadly black
shadow wavering down upon me? Already I was unable to stand
erect, when my eye caught something which brought a gush of hope
back to my heart.
"I have said that though the floor and ceiling were of iron, the
walls were of wood. As I gave a last hurried glance around, I saw
a thin line of yellow light between two of the boards, which
broadened and broadened as a small panel was pushed backward. For
an instant I could hardly believe that here was indeed a door
which led away from death. The next instant I threw myself
through, and lay half-fainting upon the other side. The panel had
closed again behind me, but the crash of the lamp, and a few
moments afterwards the clang of the two slabs of metal, told me
how narrow had been my escape.
"I was recalled to myself by a frantic plucking at my wrist, and
I found myself lying upon the stone floor of a narrow corridor,
while a woman bent over me and tugged at me with her left hand,
while she held a candle in her right. It was the same good friend
whose warning I had so foolishly rejected.
"'Come! come!' she cried breathlessly. 'They will be here in a
moment. They will see that you are not there. Oh, do not waste
the so-precious time, but come!'
"This time, at least, I did not scorn her advice. I staggered to
my feet and ran with her along the corridor and down a winding
stair. The latter led to another broad passage, and just as we
reached it we heard the sound of running feet and the shouting of
two voices, one answering the other from the floor on which we
were and from the one beneath. My guide stopped and looked about
her like one who is at her wit's end. Then she threw open a door
which led into a bedroom, through the window of which the moon
was shining brightly.
"'It is your only chance,' said she. 'It is high, but it may be
that you can jump it.'
"As she spoke a light sprang into view at the further end of the
passage, and I saw the lean figure of Colonel Lysander Stark
rushing forward with a lantern in one hand and a weapon like a
butcher's cleaver in the other. I rushed across the bedroom,
flung open the window, and looked out. How quiet and sweet and
wholesome the garden looked in the moonlight, and it could not be
more than thirty feet down. I clambered out upon the sill, but I
hesitated to jump until I should have heard what passed between
my saviour and the ruffian who pursued me. If she were ill-used,
then at any risks I was determined to go back to her assistance.
The thought had hardly flashed through my mind before he was at
the door, pushing his way past her; but she threw her arms round
him and tried to hold him back.
"'Fritz! Fritz!' she cried in English, 'remember your promise
after the last time. You said it should not be again. He will be
silent! Oh, he will be silent!'
"'You are mad, Elise!' he shouted, struggling to break away from
her. 'You will be the ruin of us. He has seen too much. Let me
pass, I say!' He dashed her to one side, and, rushing to the
window, cut at me with his heavy weapon. I had let myself go, and
was hanging by the hands to the sill, when his blow fell. I was
conscious of a dull pain, my grip loosened, and I fell into the
garden below.
"I was shaken but not hurt by the fall; so I picked myself up and
rushed off among the bushes as hard as I could run, for I
understood that I was far from being out of danger yet. Suddenly,
however, as I ran, a deadly dizziness and sickness came over me.
I glanced down at my hand, which was throbbing painfully, and
then, for the first time, saw that my thumb had been cut off and
that the blood was pouring from my wound. I endeavoured to tie my
handkerchief round it, but there came a sudden buzzing in my
ears, and next moment I fell in a dead faint among the
rose-bushes.
"How long I remained unconscious I cannot tell. It must have been
a very long time, for the moon had sunk, and a bright morning was
breaking when I came to myself. My clothes were all sodden with
dew, and my coat-sleeve was drenched with blood from my wounded
thumb. The smarting of it recalled in an instant all the
particulars of my night's adventure, and I sprang to my feet with
the feeling that I might hardly yet be safe from my pursuers. But
to my astonishment, when I came to look round me, neither house
nor garden were to be seen. I had been lying in an angle of the
hedge close by the highroad, and just a little lower down was a
long building, which proved, upon my approaching it, to be the
very station at which I had arrived upon the previous night. Were
it not for the ugly wound upon my hand, all that had passed
during those dreadful hours might have been an evil dream.
"Half dazed, I went into the station and asked about the morning
train. There would be one to Reading in less than an hour. The
same porter was on duty, I found, as had been there when I
arrived. I inquired of him whether he had ever heard of Colonel
Lysander Stark. The name was strange to him. Had he observed a
carriage the night before waiting for me? No, he had not. Was
there a police-station anywhere near? There was one about three
miles off.
"It was too far for me to go, weak and ill as I was. I determined
to wait until I got back to town before telling my story to the
police. It was a little past six when I arrived, so I went first
to have my wound dressed, and then the doctor was kind enough to
bring me along here. I put the case into your hands and shall do
exactly what you advise."
We both sat in silence for some little time after listening to
this extraordinary narrative. Then Sherlock Holmes pulled down
from the shelf one of the ponderous commonplace books in which he
placed his cuttings.
"Here is an advertisement which will interest you," said he. "It
appeared in all the papers about a year ago. Listen to this:
'Lost, on the 9th inst., Mr. Jeremiah Hayling, aged
twenty-six, a hydraulic engineer. Left his lodgings at ten
o'clock at night, and has not been heard of since. Was
dressed in,' etc., etc. Ha! That represents the last time that
the colonel needed to have his machine overhauled, I fancy."
"Good heavens!" cried my patient. "Then that explains what the
girl said."
"Undoubtedly. It is quite clear that the colonel was a cool and
desperate man, who was absolutely determined that nothing should
stand in the way of his little game, like those out-and-out
pirates who will leave no survivor from a captured ship. Well,
every moment now is precious, so if you feel equal to it we shall
go down to Scotland Yard at once as a preliminary to starting for
Eyford."
Some three hours or so afterwards we were all in the train
together, bound from Reading to the little Berkshire village.
There were Sherlock Holmes, the hydraulic engineer, Inspector
Bradstreet, of Scotland Yard, a plain-clothes man, and myself.
Bradstreet had spread an ordnance map of the county out upon the
seat and was busy with his compasses drawing a circle with Eyford
for its centre.
"There you are," said he. "That circle is drawn at a radius of
ten miles from the village. The place we want must be somewhere
near that line. You said ten miles, I think, sir."
"It was an hour's good drive."
"And you think that they brought you back all that way when you
were unconscious?"
"They must have done so. I have a confused memory, too, of having
been lifted and conveyed somewhere."
"What I cannot understand," said I, "is why they should have
spared you when they found you lying fainting in the garden.
Perhaps the villain was softened by the woman's entreaties."
"I hardly think that likely. I never saw a more inexorable face
in my life."
"Oh, we shall soon clear up all that," said Bradstreet. "Well, I
have drawn my circle, and I only wish I knew at what point upon
it the folk that we are in search of are to be found."
"I think I could lay my finger on it," said Holmes quietly.
"Really, now!" cried the inspector, "you have formed your
opinion! Come, now, we shall see who agrees with you. I say it is
south, for the country is more deserted there."
"And I say east," said my patient.
"I am for west," remarked the plain-clothes man. "There are
several quiet little villages up there."
"And I am for north," said I, "because there are no hills there,
and our friend says that he did not notice the carriage go up
any."
"Come," cried the inspector, laughing; "it's a very pretty
diversity of opinion. We have boxed the compass among us. Who do
you give your casting vote to?"
"You are all wrong."
"But we can't all be."
"Oh, yes, you can. This is my point." He placed his finger in the
centre of the circle. "This is where we shall find them."
"But the twelve-mile drive?" gasped Hatherley.
"Six out and six back. Nothing simpler. You say yourself that the
horse was fresh and glossy when you got in. How could it be that
if it had gone twelve miles over heavy roads?"
"Indeed, it is a likely ruse enough," observed Bradstreet
thoughtfully. "Of course there can be no doubt as to the nature
of this gang."
"None at all," said Holmes. "They are coiners on a large scale,
and have used the machine to form the amalgam which has taken the
place of silver."
"We have known for some time that a clever gang was at work,"
said the inspector. "They have been turning out half-crowns by
the thousand. We even traced them as far as Reading, but could
get no farther, for they had covered their traces in a way that
showed that they were very old hands. But now, thanks to this
lucky chance, I think that we have got them right enough."
But the inspector was mistaken, for those criminals were not
destined to fall into the hands of justice. As we rolled into
Eyford Station we saw a gigantic column of smoke which streamed
up from behind a small clump of trees in the neighbourhood and
hung like an immense ostrich feather over the landscape.
"A house on fire?" asked Bradstreet as the train steamed off
again on its way.
"Yes, sir!" said the station-master.
"When did it break out?"
"I hear that it was during the night, sir, but it has got worse,
and the whole place is in a blaze."
"Whose house is it?"
"Dr. Becher's."
"Tell me," broke in the engineer, "is Dr. Becher a German, very
thin, with a long, sharp nose?"
The station-master laughed heartily. "No, sir, Dr. Becher is an
Englishman, and there isn't a man in the parish who has a
better-lined waistcoat. But he has a gentleman staying with him,
a patient, as I understand, who is a foreigner, and he looks as
if a little good Berkshire beef would do him no harm."
The station-master had not finished his speech before we were all
hastening in the direction of the fire. The road topped a low
hill, and there was a great widespread whitewashed building in
front of us, spouting fire at every chink and window, while in
the garden in front three fire-engines were vainly striving to
keep the flames under.
"That's it!" cried Hatherley, in intense excitement. "There is
the gravel-drive, and there are the rose-bushes where I lay. That
second window is the one that I jumped from."
"Well, at least," said Holmes, "you have had your revenge upon
them. There can be no question that it was your oil-lamp which,
when it was crushed in the press, set fire to the wooden walls,
though no doubt they were too excited in the chase after you to
observe it at the time. Now keep your eyes open in this crowd for
your friends of last night, though I very much fear that they are
a good hundred miles off by now."
And Holmes' fears came to be realised, for from that day to this
no word has ever been heard either of the beautiful woman, the
sinister German, or the morose Englishman. Early that morning a
peasant had met a cart containing several people and some very
bulky boxes driving rapidly in the direction of Reading, but
there all traces of the fugitives disappeared, and even Holmes'
ingenuity failed ever to discover the least clue as to their
whereabouts.
The firemen had been much perturbed at the strange arrangements
which they had found within, and still more so by discovering a
newly severed human thumb upon a window-sill of the second floor.
About sunset, however, their efforts were at last successful, and
they subdued the flames, but not before the roof had fallen in,
and the whole place been reduced to such absolute ruin that, save
some twisted cylinders and iron piping, not a trace remained of
the machinery which had cost our unfortunate acquaintance so
dearly. Large masses of nickel and of tin were discovered stored
in an out-house, but no coins were to be found, which may have
explained the presence of those bulky boxes which have been
already referred to.
How our hydraulic engineer had been conveyed from the garden to
the spot where he recovered his senses might have remained
forever a mystery were it not for the soft mould, which told us a
very plain tale. He had evidently been carried down by two
persons, one of whom had remarkably small feet and the other
unusually large ones. On the whole, it was most probable that the
silent Englishman, being less bold or less murderous than his
companion, had assisted the woman to bear the unconscious man out
of the way of danger.
"Well," said our engineer ruefully as we took our seats to return
once more to London, "it has been a pretty business for me! I
have lost my thumb and I have lost a fifty-guinea fee, and what
have I gained?"
"Experience," said Holmes, laughing. "Indirectly it may be of
value, you know; you have only to put it into words to gain the
reputation of being excellent company for the remainder of your
existence."
X. THE ADVENTURE OF THE NOBLE BACHELOR
The Lord St. Simon marriage, and its curious termination, have
long ceased to be a subject of interest in those exalted circles
in which the unfortunate bridegroom moves. Fresh scandals have
eclipsed it, and their more piquant details have drawn the
gossips away from this four-year-old drama. As I have reason to
believe, however, that the full facts have never been revealed to
the general public, and as my friend Sherlock Holmes had a
considerable share in clearing the matter up, I feel that no
memoir of him would be complete without some little sketch of
this remarkable episode.
It was a few weeks before my own marriage, during the days when I
was still sharing rooms with Holmes in Baker Street, that he came
home from an afternoon stroll to find a letter on the table
waiting for him. I had remained indoors all day, for the weather
had taken a sudden turn to rain, with high autumnal winds, and
the Jezail bullet which I had brought back in one of my limbs as
a relic of my Afghan campaign throbbed with dull persistence.
With my body in one easy-chair and my legs upon another, I had
surrounded myself with a cloud of newspapers until at last,
saturated with the news of the day, I tossed them all aside and
lay listless, watching the huge crest and monogram upon the
envelope upon the table and wondering lazily who my friend's
noble correspondent could be.
"Here is a very fashionable epistle," I remarked as he entered.
"Your morning letters, if I remember right, were from a
fish-monger and a tide-waiter."
"Yes, my correspondence has certainly the charm of variety," he
answered, smiling, "and the humbler are usually the more
interesting. This looks like one of those unwelcome social
summonses which call upon a man either to be bored or to lie."
He broke the seal and glanced over the contents.
"Oh, come, it may prove to be something of interest, after all."
"Not social, then?"
"No, distinctly professional."
"And from a noble client?"
"One of the highest in England."
"My dear fellow, I congratulate you."
"I assure you, Watson, without affectation, that the status of my
client is a matter of less moment to me than the interest of his
case. It is just possible, however, that that also may not be
wanting in this new investigation. You have been reading the
papers diligently of late, have you not?"
"It looks like it," said I ruefully, pointing to a huge bundle in
the corner. "I have had nothing else to do."
"It is fortunate, for you will perhaps be able to post me up. I
read nothing except the criminal news and the agony column. The
latter is always instructive. But if you have followed recent
events so closely you must have read about Lord St. Simon and his
wedding?"
"Oh, yes, with the deepest interest."
"That is well. The letter which I hold in my hand is from Lord
St. Simon. I will read it to you, and in return you must turn
over these papers and let me have whatever bears upon the matter.
This is what he says:
"'MY DEAR MR. SHERLOCK HOLMES:--Lord Backwater tells me that I
may place implicit reliance upon your judgment and discretion. I
have determined, therefore, to call upon you and to consult you
in reference to the very painful event which has occurred in
connection with my wedding. Mr. Lestrade, of Scotland Yard, is
acting already in the matter, but he assures me that he sees no
objection to your co-operation, and that he even thinks that
it might be of some assistance. I will call at four o'clock in
the afternoon, and, should you have any other engagement at that
time, I hope that you will postpone it, as this matter is of
paramount importance. Yours faithfully, ST. SIMON.'
"It is dated from Grosvenor Mansions, written with a quill pen,
and the noble lord has had the misfortune to get a smear of ink
upon the outer side of his right little finger," remarked Holmes
as he folded up the epistle.
"He says four o'clock. It is three now. He will be here in an
hour."
"Then I have just time, with your assistance, to get clear upon
the subject. Turn over those papers and arrange the extracts in
their order of time, while I take a glance as to who our client
is." He picked a red-covered volume from a line of books of
reference beside the mantelpiece. "Here he is," said he, sitting
down and flattening it out upon his knee. "'Lord Robert Walsingham
de Vere St. Simon, second son of the Duke of Balmoral.' Hum! 'Arms:
Azure, three caltrops in chief over a fess sable. Born in 1846.'
He's forty-one years of age, which is mature for marriage. Was
Under-Secretary for the colonies in a late administration. The
Duke, his father, was at one time Secretary for Foreign Affairs.
They inherit Plantagenet blood by direct descent, and Tudor on
the distaff side. Ha! Well, there is nothing very instructive in
all this. I think that I must turn to you Watson, for something
more solid."
"I have very little difficulty in finding what I want," said I,
"for the facts are quite recent, and the matter struck me as
remarkable. I feared to refer them to you, however, as I knew
that you had an inquiry on hand and that you disliked the
intrusion of other matters."
"Oh, you mean the little problem of the Grosvenor Square
furniture van. That is quite cleared up now--though, indeed, it
was obvious from the first. Pray give me the results of your
newspaper selections."
"Here is the first notice which I can find. It is in the personal
column of the Morning Post, and dates, as you see, some weeks
back: 'A marriage has been arranged,' it says, 'and will, if
rumour is correct, very shortly take place, between Lord Robert
St. Simon, second son of the Duke of Balmoral, and Miss Hatty
Doran, the only daughter of Aloysius Doran. Esq., of San
Francisco, Cal., U.S.A.' That is all."
"Terse and to the point," remarked Holmes, stretching his long,
thin legs towards the fire.
"There was a paragraph amplifying this in one of the society
papers of the same week. Ah, here it is: 'There will soon be a
call for protection in the marriage market, for the present
free-trade principle appears to tell heavily against our home
product. One by one the management of the noble houses of Great
Britain is passing into the hands of our fair cousins from across
the Atlantic. An important addition has been made during the last
week to the list of the prizes which have been borne away by
these charming invaders. Lord St. Simon, who has shown himself
for over twenty years proof against the little god's arrows, has
now definitely announced his approaching marriage with Miss Hatty
Doran, the fascinating daughter of a California millionaire. Miss
Doran, whose graceful figure and striking face attracted much
attention at the Westbury House festivities, is an only child,
and it is currently reported that her dowry will run to
considerably over the six figures, with expectancies for the
future. As it is an open secret that the Duke of Balmoral has
been compelled to sell his pictures within the last few years,
and as Lord St. Simon has no property of his own save the small
estate of Birchmoor, it is obvious that the Californian heiress
is not the only gainer by an alliance which will enable her to
make the easy and common transition from a Republican lady to a
British peeress.'"
"Anything else?" asked Holmes, yawning.
"Oh, yes; plenty. Then there is another note in the Morning Post
to say that the marriage would be an absolutely quiet one, that it
would be at St. George's, Hanover Square, that only half a dozen
intimate friends would be invited, and that the party would
return to the furnished house at Lancaster Gate which has been
taken by Mr. Aloysius Doran. Two days later--that is, on
Wednesday last--there is a curt announcement that the wedding had
taken place, and that the honeymoon would be passed at Lord
Backwater's place, near Petersfield. Those are all the notices
which appeared before the disappearance of the bride."
"Before the what?" asked Holmes with a start.
"The vanishing of the lady."
"When did she vanish, then?"
"At the wedding breakfast."
"Indeed. This is more interesting than it promised to be; quite
dramatic, in fact."
"Yes; it struck me as being a little out of the common."
"They often vanish before the ceremony, and occasionally during
the honeymoon; but I cannot call to mind anything quite so prompt
as this. Pray let me have the details."
"I warn you that they are very incomplete."
"Perhaps we may make them less so."
"Such as they are, they are set forth in a single article of a
morning paper of yesterday, which I will read to you. It is
headed, 'Singular Occurrence at a Fashionable Wedding':
"'The family of Lord Robert St. Simon has been thrown into the
greatest consternation by the strange and painful episodes which
have taken place in connection with his wedding. The ceremony, as
shortly announced in the papers of yesterday, occurred on the
previous morning; but it is only now that it has been possible to
confirm the strange rumours which have been so persistently
floating about. In spite of the attempts of the friends to hush
the matter up, so much public attention has now been drawn to it
that no good purpose can be served by affecting to disregard what
is a common subject for conversation.
"'The ceremony, which was performed at St. George's, Hanover
Square, was a very quiet one, no one being present save the
father of the bride, Mr. Aloysius Doran, the Duchess of Balmoral,
Lord Backwater, Lord Eustace and Lady Clara St. Simon (the
younger brother and sister of the bridegroom), and Lady Alicia
Whittington. The whole party proceeded afterwards to the house of
Mr. Aloysius Doran, at Lancaster Gate, where breakfast had been
prepared. It appears that some little trouble was caused by a
woman, whose name has not been ascertained, who endeavoured to
force her way into the house after the bridal party, alleging
that she had some claim upon Lord St. Simon. It was only after a
painful and prolonged scene that she was ejected by the butler
and the footman. The bride, who had fortunately entered the house
before this unpleasant interruption, had sat down to breakfast
with the rest, when she complained of a sudden indisposition and
retired to her room. Her prolonged absence having caused some
comment, her father followed her, but learned from her maid that
she had only come up to her chamber for an instant, caught up an
ulster and bonnet, and hurried down to the passage. One of the
footmen declared that he had seen a lady leave the house thus
apparelled, but had refused to credit that it was his mistress,
believing her to be with the company. On ascertaining that his
daughter had disappeared, Mr. Aloysius Doran, in conjunction with
the bridegroom, instantly put themselves in communication with
the police, and very energetic inquiries are being made, which
will probably result in a speedy clearing up of this very
singular business. Up to a late hour last night, however, nothing
had transpired as to the whereabouts of the missing lady. There
are rumours of foul play in the matter, and it is said that the
police have caused the arrest of the woman who had caused the
original disturbance, in the belief that, from jealousy or some
other motive, she may have been concerned in the strange
disappearance of the bride.'"
"And is that all?"
"Only one little item in another of the morning papers, but it is
a suggestive one."
"And it is--"
"That Miss Flora Millar, the lady who had caused the disturbance,
has actually been arrested. It appears that she was formerly a
danseuse at the Allegro, and that she has known the bridegroom
for some years. There are no further particulars, and the whole
case is in your hands now--so far as it has been set forth in the
public press."
"And an exceedingly interesting case it appears to be. I would
not have missed it for worlds. But there is a ring at the bell,
Watson, and as the clock makes it a few minutes after four, I
have no doubt that this will prove to be our noble client. Do not
dream of going, Watson, for I very much prefer having a witness,
if only as a check to my own memory."
"Lord Robert St. Simon," announced our page-boy, throwing open
the door. A gentleman entered, with a pleasant, cultured face,
high-nosed and pale, with something perhaps of petulance about
the mouth, and with the steady, well-opened eye of a man whose
pleasant lot it had ever been to command and to be obeyed. His
manner was brisk, and yet his general appearance gave an undue
impression of age, for he had a slight forward stoop and a little
bend of the knees as he walked. His hair, too, as he swept off
his very curly-brimmed hat, was grizzled round the edges and thin
upon the top. As to his dress, it was careful to the verge of
foppishness, with high collar, black frock-coat, white waistcoat,
yellow gloves, patent-leather shoes, and light-coloured gaiters.
He advanced slowly into the room, turning his head from left to
right, and swinging in his right hand the cord which held his
golden eyeglasses.
"Good-day, Lord St. Simon," said Holmes, rising and bowing. "Pray
take the basket-chair. This is my friend and colleague, Dr.
Watson. Draw up a little to the fire, and we will talk this
matter over."
"A most painful matter to me, as you can most readily imagine,
Mr. Holmes. I have been cut to the quick. I understand that you
have already managed several delicate cases of this sort, sir,
though I presume that they were hardly from the same class of
society."
"No, I am descending."
"I beg pardon."
"My last client of the sort was a king."
"Oh, really! I had no idea. And which king?"
"The King of Scandinavia."
"What! Had he lost his wife?"
"You can understand," said Holmes suavely, "that I extend to the
affairs of my other clients the same secrecy which I promise to
you in yours."
"Of course! Very right! very right! I'm sure I beg pardon. As to
my own case, I am ready to give you any information which may
assist you in forming an opinion."
"Thank you. I have already learned all that is in the public
prints, nothing more. I presume that I may take it as correct--this
article, for example, as to the disappearance of the bride."
Lord St. Simon glanced over it. "Yes, it is correct, as far as it
goes."
"But it needs a great deal of supplementing before anyone could
offer an opinion. I think that I may arrive at my facts most
directly by questioning you."
"Pray do so."
"When did you first meet Miss Hatty Doran?"
"In San Francisco, a year ago."
"You were travelling in the States?"
"Yes."
"Did you become engaged then?"
"No."
"But you were on a friendly footing?"
"I was amused by her society, and she could see that I was
amused."
"Her father is very rich?"
"He is said to be the richest man on the Pacific slope."
"And how did he make his money?"
"In mining. He had nothing a few years ago. Then he struck gold,
invested it, and came up by leaps and bounds."
"Now, what is your own impression as to the young lady's--your
wife's character?"
The nobleman swung his glasses a little faster and stared down
into the fire. "You see, Mr. Holmes," said he, "my wife was
twenty before her father became a rich man. During that time she
ran free in a mining camp and wandered through woods or
mountains, so that her education has come from Nature rather than
from the schoolmaster. She is what we call in England a tomboy,
with a strong nature, wild and free, unfettered by any sort of
traditions. She is impetuous--volcanic, I was about to say. She
is swift in making up her mind and fearless in carrying out her
resolutions. On the other hand, I would not have given her the
name which I have the honour to bear"--he gave a little stately
cough--"had not I thought her to be at bottom a noble woman. I
believe that she is capable of heroic self-sacrifice and that
anything dishonourable would be repugnant to her."
"Have you her photograph?"
"I brought this with me." He opened a locket and showed us the
full face of a very lovely woman. It was not a photograph but an
ivory miniature, and the artist had brought out the full effect
of the lustrous black hair, the large dark eyes, and the
exquisite mouth. Holmes gazed long and earnestly at it. Then he
closed the locket and handed it back to Lord St. Simon.
"The young lady came to London, then, and you renewed your
acquaintance?"
"Yes, her father brought her over for this last London season. I
met her several times, became engaged to her, and have now
married her."
"She brought, I understand, a considerable dowry?"
"A fair dowry. Not more than is usual in my family."
"And this, of course, remains to you, since the marriage is a
fait accompli?"
"I really have made no inquiries on the subject."
"Very naturally not. Did you see Miss Doran on the day before the
wedding?"
"Yes."
"Was she in good spirits?"
"Never better. She kept talking of what we should do in our
future lives."
"Indeed! That is very interesting. And on the morning of the
wedding?"
"She was as bright as possible--at least until after the
ceremony."
"And did you observe any change in her then?"
"Well, to tell the truth, I saw then the first signs that I had
ever seen that her temper was just a little sharp. The incident
however, was too trivial to relate and can have no possible
bearing upon the case."
"Pray let us have it, for all that."
"Oh, it is childish. She dropped her bouquet as we went towards
the vestry. She was passing the front pew at the time, and it
fell over into the pew. There was a moment's delay, but the
gentleman in the pew handed it up to her again, and it did not
appear to be the worse for the fall. Yet when I spoke to her of
the matter, she answered me abruptly; and in the carriage, on our
way home, she seemed absurdly agitated over this trifling cause."
"Indeed! You say that there was a gentleman in the pew. Some of
the general public were present, then?"
"Oh, yes. It is impossible to exclude them when the church is
open."
"This gentleman was not one of your wife's friends?"
"No, no; I call him a gentleman by courtesy, but he was quite a
common-looking person. I hardly noticed his appearance. But
really I think that we are wandering rather far from the point."
"Lady St. Simon, then, returned from the wedding in a less
cheerful frame of mind than she had gone to it. What did she do
on re-entering her father's house?"
"I saw her in conversation with her maid."
"And who is her maid?"
"Alice is her name. She is an American and came from California
with her."
"A confidential servant?"
"A little too much so. It seemed to me that her mistress allowed
her to take great liberties. Still, of course, in America they
look upon these things in a different way."
"How long did she speak to this Alice?"
"Oh, a few minutes. I had something else to think of."
"You did not overhear what they said?"
"Lady St. Simon said something about 'jumping a claim.' She was
accustomed to use slang of the kind. I have no idea what she
meant."
"American slang is very expressive sometimes. And what did your
wife do when she finished speaking to her maid?"
"She walked into the breakfast-room."
"On your arm?"
"No, alone. She was very independent in little matters like that.
Then, after we had sat down for ten minutes or so, she rose
hurriedly, muttered some words of apology, and left the room. She
never came back."
"But this maid, Alice, as I understand, deposes that she went to
her room, covered her bride's dress with a long ulster, put on a
bonnet, and went out."
"Quite so. And she was afterwards seen walking into Hyde Park in
company with Flora Millar, a woman who is now in custody, and who
had already made a disturbance at Mr. Doran's house that
morning."
"Ah, yes. I should like a few particulars as to this young lady,
and your relations to her."
Lord St. Simon shrugged his shoulders and raised his eyebrows.
"We have been on a friendly footing for some years--I may say on
a very friendly footing. She used to be at the Allegro. I have
not treated her ungenerously, and she had no just cause of
complaint against me, but you know what women are, Mr. Holmes.
Flora was a dear little thing, but exceedingly hot-headed and
devotedly attached to me. She wrote me dreadful letters when she
heard that I was about to be married, and, to tell the truth, the
reason why I had the marriage celebrated so quietly was that I
feared lest there might be a scandal in the church. She came to
Mr. Doran's door just after we returned, and she endeavoured to
push her way in, uttering very abusive expressions towards my
wife, and even threatening her, but I had foreseen the
possibility of something of the sort, and I had two police
fellows there in private clothes, who soon pushed her out again.
She was quiet when she saw that there was no good in making a
row."
"Did your wife hear all this?"
"No, thank goodness, she did not."
"And she was seen walking with this very woman afterwards?"
"Yes. That is what Mr. Lestrade, of Scotland Yard, looks upon as
so serious. It is thought that Flora decoyed my wife out and laid
some terrible trap for her."
"Well, it is a possible supposition."
"You think so, too?"
"I did not say a probable one. But you do not yourself look upon
this as likely?"
"I do not think Flora would hurt a fly."
"Still, jealousy is a strange transformer of characters. Pray
what is your own theory as to what took place?"
"Well, really, I came to seek a theory, not to propound one. I
have given you all the facts. Since you ask me, however, I may
say that it has occurred to me as possible that the excitement of
this affair, the consciousness that she had made so immense a
social stride, had the effect of causing some little nervous
disturbance in my wife."
"In short, that she had become suddenly deranged?"
"Well, really, when I consider that she has turned her back--I
will not say upon me, but upon so much that many have aspired to
without success--I can hardly explain it in any other fashion."
"Well, certainly that is also a conceivable hypothesis," said
Holmes, smiling. "And now, Lord St. Simon, I think that I have
nearly all my data. May I ask whether you were seated at the
breakfast-table so that you could see out of the window?"
"We could see the other side of the road and the Park."
"Quite so. Then I do not think that I need to detain you longer.
I shall communicate with you."
"Should you be fortunate enough to solve this problem," said our
client, rising.
"I have solved it."
"Eh? What was that?"
"I say that I have solved it."
"Where, then, is my wife?"
"That is a detail which I shall speedily supply."
Lord St. Simon shook his head. "I am afraid that it will take
wiser heads than yours or mine," he remarked, and bowing in a
stately, old-fashioned manner he departed.
"It is very good of Lord St. Simon to honour my head by putting
it on a level with his own," said Sherlock Holmes, laughing. "I
think that I shall have a whisky and soda and a cigar after all
this cross-questioning. I had formed my conclusions as to the
case before our client came into the room."
"My dear Holmes!"
"I have notes of several similar cases, though none, as I
remarked before, which were quite as prompt. My whole examination
served to turn my conjecture into a certainty. Circumstantial
evidence is occasionally very convincing, as when you find a
trout in the milk, to quote Thoreau's example."
"But I have heard all that you have heard."
"Without, however, the knowledge of pre-existing cases which
serves me so well. There was a parallel instance in Aberdeen some
years back, and something on very much the same lines at Munich
the year after the Franco-Prussian War. It is one of these
cases--but, hullo, here is Lestrade! Good-afternoon, Lestrade!
You will find an extra tumbler upon the sideboard, and there are
cigars in the box."
The official detective was attired in a pea-jacket and cravat,
which gave him a decidedly nautical appearance, and he carried a
black canvas bag in his hand. With a short greeting he seated
himself and lit the cigar which had been offered to him.
"What's up, then?" asked Holmes with a twinkle in his eye. "You
look dissatisfied."
"And I feel dissatisfied. It is this infernal St. Simon marriage
case. I can make neither head nor tail of the business."
"Really! You surprise me."
"Who ever heard of such a mixed affair? Every clue seems to slip
through my fingers. I have been at work upon it all day."
"And very wet it seems to have made you," said Holmes laying his
hand upon the arm of the pea-jacket.
"Yes, I have been dragging the Serpentine."
"In heaven's name, what for?"
"In search of the body of Lady St. Simon."
Sherlock Holmes leaned back in his chair and laughed heartily.
"Have you dragged the basin of Trafalgar Square fountain?" he
asked.
"Why? What do you mean?"
"Because you have just as good a chance of finding this lady in
the one as in the other."
Lestrade shot an angry glance at my companion. "I suppose you
know all about it," he snarled.
"Well, I have only just heard the facts, but my mind is made up."
"Oh, indeed! Then you think that the Serpentine plays no part in
the matter?"
"I think it very unlikely."
"Then perhaps you will kindly explain how it is that we found
this in it?" He opened his bag as he spoke, and tumbled onto the
floor a wedding-dress of watered silk, a pair of white satin
shoes and a bride's wreath and veil, all discoloured and soaked
in water. "There," said he, putting a new wedding-ring upon the
top of the pile. "There is a little nut for you to crack, Master
Holmes."
"Oh, indeed!" said my friend, blowing blue rings into the air.
"You dragged them from the Serpentine?"
"No. They were found floating near the margin by a park-keeper.
They have been identified as her clothes, and it seemed to me
that if the clothes were there the body would not be far off."
"By the same brilliant reasoning, every man's body is to be found
in the neighbourhood of his wardrobe. And pray what did you hope
to arrive at through this?"
"At some evidence implicating Flora Millar in the disappearance."
"I am afraid that you will find it difficult."
"Are you, indeed, now?" cried Lestrade with some bitterness. "I
am afraid, Holmes, that you are not very practical with your
deductions and your inferences. You have made two blunders in as
many minutes. This dress does implicate Miss Flora Millar."
"And how?"
"In the dress is a pocket. In the pocket is a card-case. In the
card-case is a note. And here is the very note." He slapped it
down upon the table in front of him. "Listen to this: 'You will
see me when all is ready. Come at once. F.H.M.' Now my theory all
along has been that Lady St. Simon was decoyed away by Flora
Millar, and that she, with confederates, no doubt, was
responsible for her disappearance. Here, signed with her
initials, is the very note which was no doubt quietly slipped
into her hand at the door and which lured her within their
reach."
"Very good, Lestrade," said Holmes, laughing. "You really are
very fine indeed. Let me see it." He took up the paper in a
listless way, but his attention instantly became riveted, and he
gave a little cry of satisfaction. "This is indeed important,"
said he.
"Ha! you find it so?"
"Extremely so. I congratulate you warmly."
Lestrade rose in his triumph and bent his head to look. "Why," he
shrieked, "you're looking at the wrong side!"
"On the contrary, this is the right side."
"The right side? You're mad! Here is the note written in pencil
over here."
"And over here is what appears to be the fragment of a hotel
bill, which interests me deeply."
"There's nothing in it. I looked at it before," said Lestrade.
"'Oct. 4th, rooms 8s., breakfast 2s. 6d., cocktail 1s., lunch 2s.
6d., glass sherry, 8d.' I see nothing in that."
"Very likely not. It is most important, all the same. As to the
note, it is important also, or at least the initials are, so I
congratulate you again."
"I've wasted time enough," said Lestrade, rising. "I believe in
hard work and not in sitting by the fire spinning fine theories.
Good-day, Mr. Holmes, and we shall see which gets to the bottom
of the matter first." He gathered up the garments, thrust them
into the bag, and made for the door.
"Just one hint to you, Lestrade," drawled Holmes before his rival
vanished; "I will tell you the true solution of the matter. Lady
St. Simon is a myth. There is not, and there never has been, any
such person."
Lestrade looked sadly at my companion. Then he turned to me,
tapped his forehead three times, shook his head solemnly, and
hurried away.
He had hardly shut the door behind him when Holmes rose to put on
his overcoat. "There is something in what the fellow says about
outdoor work," he remarked, "so I think, Watson, that I must
leave you to your papers for a little."
It was after five o'clock when Sherlock Holmes left me, but I had
no time to be lonely, for within an hour there arrived a
confectioner's man with a very large flat box. This he unpacked
with the help of a youth whom he had brought with him, and
presently, to my very great astonishment, a quite epicurean
little cold supper began to be laid out upon our humble
lodging-house mahogany. There were a couple of brace of cold
woodcock, a pheasant, a pâté de foie gras pie with a group of
ancient and cobwebby bottles. Having laid out all these luxuries,
my two visitors vanished away, like the genii of the Arabian
Nights, with no explanation save that the things had been paid
for and were ordered to this address.
Just before nine o'clock Sherlock Holmes stepped briskly into the
room. His features were gravely set, but there was a light in his
eye which made me think that he had not been disappointed in his
conclusions.
"They have laid the supper, then," he said, rubbing his hands.
"You seem to expect company. They have laid for five."
"Yes, I fancy we may have some company dropping in," said he. "I
am surprised that Lord St. Simon has not already arrived. Ha! I
fancy that I hear his step now upon the stairs."
It was indeed our visitor of the afternoon who came bustling in,
dangling his glasses more vigorously than ever, and with a very
perturbed expression upon his aristocratic features.
"My messenger reached you, then?" asked Holmes.
"Yes, and I confess that the contents startled me beyond measure.
Have you good authority for what you say?"
"The best possible."
Lord St. Simon sank into a chair and passed his hand over his
forehead.
"What will the Duke say," he murmured, "when he hears that one of
the family has been subjected to such humiliation?"
"It is the purest accident. I cannot allow that there is any
humiliation."
"Ah, you look on these things from another standpoint."
"I fail to see that anyone is to blame. I can hardly see how the
lady could have acted otherwise, though her abrupt method of
doing it was undoubtedly to be regretted. Having no mother, she
had no one to advise her at such a crisis."
"It was a slight, sir, a public slight," said Lord St. Simon,
tapping his fingers upon the table.
"You must make allowance for this poor girl, placed in so
unprecedented a position."
"I will make no allowance. I am very angry indeed, and I have
been shamefully used."
"I think that I heard a ring," said Holmes. "Yes, there are steps
on the landing. If I cannot persuade you to take a lenient view
of the matter, Lord St. Simon, I have brought an advocate here
who may be more successful." He opened the door and ushered in a
lady and gentleman. "Lord St. Simon," said he "allow me to
introduce you to Mr. and Mrs. Francis Hay Moulton. The lady, I
think, you have already met."
At the sight of these newcomers our client had sprung from his
seat and stood very erect, with his eyes cast down and his hand
thrust into the breast of his frock-coat, a picture of offended
dignity. The lady had taken a quick step forward and had held out
her hand to him, but he still refused to raise his eyes. It was
as well for his resolution, perhaps, for her pleading face was
one which it was hard to resist.
"You're angry, Robert," said she. "Well, I guess you have every
cause to be."
"Pray make no apology to me," said Lord St. Simon bitterly.
"Oh, yes, I know that I have treated you real bad and that I
should have spoken to you before I went; but I was kind of
rattled, and from the time when I saw Frank here again I just
didn't know what I was doing or saying. I only wonder I didn't
fall down and do a faint right there before the altar."
"Perhaps, Mrs. Moulton, you would like my friend and me to leave
the room while you explain this matter?"
"If I may give an opinion," remarked the strange gentleman,
"we've had just a little too much secrecy over this business
already. For my part, I should like all Europe and America to
hear the rights of it." He was a small, wiry, sunburnt man,
clean-shaven, with a sharp face and alert manner.
"Then I'll tell our story right away," said the lady. "Frank here
and I met in '84, in McQuire's camp, near the Rockies, where pa
was working a claim. We were engaged to each other, Frank and I;
but then one day father struck a rich pocket and made a pile,
while poor Frank here had a claim that petered out and came to
nothing. The richer pa grew the poorer was Frank; so at last pa
wouldn't hear of our engagement lasting any longer, and he took
me away to 'Frisco. Frank wouldn't throw up his hand, though; so
he followed me there, and he saw me without pa knowing anything
about it. It would only have made him mad to know, so we just
fixed it all up for ourselves. Frank said that he would go and
make his pile, too, and never come back to claim me until he had
as much as pa. So then I promised to wait for him to the end of
time and pledged myself not to marry anyone else while he lived.
'Why shouldn't we be married right away, then,' said he, 'and
then I will feel sure of you; and I won't claim to be your
husband until I come back?' Well, we talked it over, and he had
fixed it all up so nicely, with a clergyman all ready in waiting,
that we just did it right there; and then Frank went off to seek
his fortune, and I went back to pa.
"The next I heard of Frank was that he was in Montana, and then
he went prospecting in Arizona, and then I heard of him from New
Mexico. After that came a long newspaper story about how a
miners' camp had been attacked by Apache Indians, and there was
my Frank's name among the killed. I fainted dead away, and I was
very sick for months after. Pa thought I had a decline and took
me to half the doctors in 'Frisco. Not a word of news came for a
year and more, so that I never doubted that Frank was really
dead. Then Lord St. Simon came to 'Frisco, and we came to London,
and a marriage was arranged, and pa was very pleased, but I felt
all the time that no man on this earth would ever take the place
in my heart that had been given to my poor Frank.
"Still, if I had married Lord St. Simon, of course I'd have done
my duty by him. We can't command our love, but we can our
actions. I went to the altar with him with the intention to make
him just as good a wife as it was in me to be. But you may
imagine what I felt when, just as I came to the altar rails, I
glanced back and saw Frank standing and looking at me out of the
first pew. I thought it was his ghost at first; but when I looked
again there he was still, with a kind of question in his eyes, as
if to ask me whether I were glad or sorry to see him. I wonder I
didn't drop. I know that everything was turning round, and the
words of the clergyman were just like the buzz of a bee in my
ear. I didn't know what to do. Should I stop the service and make
a scene in the church? I glanced at him again, and he seemed to
know what I was thinking, for he raised his finger to his lips to
tell me to be still. Then I saw him scribble on a piece of paper,
and I knew that he was writing me a note. As I passed his pew on
the way out I dropped my bouquet over to him, and he slipped the
note into my hand when he returned me the flowers. It was only a
line asking me to join him when he made the sign to me to do so.
Of course I never doubted for a moment that my first duty was now
to him, and I determined to do just whatever he might direct.
"When I got back I told my maid, who had known him in California,
and had always been his friend. I ordered her to say nothing, but
to get a few things packed and my ulster ready. I know I ought to
have spoken to Lord St. Simon, but it was dreadful hard before
his mother and all those great people. I just made up my mind to
run away and explain afterwards. I hadn't been at the table ten
minutes before I saw Frank out of the window at the other side of
the road. He beckoned to me and then began walking into the Park.
I slipped out, put on my things, and followed him. Some woman
came talking something or other about Lord St. Simon to
me--seemed to me from the little I heard as if he had a little
secret of his own before marriage also--but I managed to get away
from her and soon overtook Frank. We got into a cab together, and
away we drove to some lodgings he had taken in Gordon Square, and
that was my true wedding after all those years of waiting. Frank
had been a prisoner among the Apaches, had escaped, came on to
'Frisco, found that I had given him up for dead and had gone to
England, followed me there, and had come upon me at last on the
very morning of my second wedding."
"I saw it in a paper," explained the American. "It gave the name
and the church but not where the lady lived."
"Then we had a talk as to what we should do, and Frank was all
for openness, but I was so ashamed of it all that I felt as if I
should like to vanish away and never see any of them again--just
sending a line to pa, perhaps, to show him that I was alive. It
was awful to me to think of all those lords and ladies sitting
round that breakfast-table and waiting for me to come back. So
Frank took my wedding-clothes and things and made a bundle of
them, so that I should not be traced, and dropped them away
somewhere where no one could find them. It is likely that we
should have gone on to Paris to-morrow, only that this good
gentleman, Mr. Holmes, came round to us this evening, though how
he found us is more than I can think, and he showed us very
clearly and kindly that I was wrong and that Frank was right, and
that we should be putting ourselves in the wrong if we were so
secret. Then he offered to give us a chance of talking to Lord
St. Simon alone, and so we came right away round to his rooms at
once. Now, Robert, you have heard it all, and I am very sorry if
I have given you pain, and I hope that you do not think very
meanly of me."
Lord St. Simon had by no means relaxed his rigid attitude, but
had listened with a frowning brow and a compressed lip to this
long narrative.
"Excuse me," he said, "but it is not my custom to discuss my most
intimate personal affairs in this public manner."
"Then you won't forgive me? You won't shake hands before I go?"
"Oh, certainly, if it would give you any pleasure." He put out
his hand and coldly grasped that which she extended to him.
"I had hoped," suggested Holmes, "that you would have joined us
in a friendly supper."
"I think that there you ask a little too much," responded his
Lordship. "I may be forced to acquiesce in these recent
developments, but I can hardly be expected to make merry over
them. I think that with your permission I will now wish you all a
very good-night." He included us all in a sweeping bow and
stalked out of the room.
"Then I trust that you at least will honour me with your
company," said Sherlock Holmes. "It is always a joy to meet an
American, Mr. Moulton, for I am one of those who believe that the
folly of a monarch and the blundering of a minister in far-gone
years will not prevent our children from being some day citizens
of the same world-wide country under a flag which shall be a
quartering of the Union Jack with the Stars and Stripes."
"The case has been an interesting one," remarked Holmes when our
visitors had left us, "because it serves to show very clearly how
simple the explanation may be of an affair which at first sight
seems to be almost inexplicable. Nothing could be more natural
than the sequence of events as narrated by this lady, and nothing
stranger than the result when viewed, for instance, by Mr.
Lestrade of Scotland Yard."
"You were not yourself at fault at all, then?"
"From the first, two facts were very obvious to me, the one that
the lady had been quite willing to undergo the wedding ceremony,
the other that she had repented of it within a few minutes of
returning home. Obviously something had occurred during the
morning, then, to cause her to change her mind. What could that
something be? She could not have spoken to anyone when she was
out, for she had been in the company of the bridegroom. Had she
seen someone, then? If she had, it must be someone from America
because she had spent so short a time in this country that she
could hardly have allowed anyone to acquire so deep an influence
over her that the mere sight of him would induce her to change
her plans so completely. You see we have already arrived, by a
process of exclusion, at the idea that she might have seen an
American. Then who could this American be, and why should he
possess so much influence over her? It might be a lover; it might
be a husband. Her young womanhood had, I knew, been spent in
rough scenes and under strange conditions. So far I had got
before I ever heard Lord St. Simon's narrative. When he told us
of a man in a pew, of the change in the bride's manner, of so
transparent a device for obtaining a note as the dropping of a
bouquet, of her resort to her confidential maid, and of her very
significant allusion to claim-jumping--which in miners' parlance
means taking possession of that which another person has a prior
claim to--the whole situation became absolutely clear. She had
gone off with a man, and the man was either a lover or was a
previous husband--the chances being in favour of the latter."
"And how in the world did you find them?"
"It might have been difficult, but friend Lestrade held
information in his hands the value of which he did not himself
know. The initials were, of course, of the highest importance,
but more valuable still was it to know that within a week he had
settled his bill at one of the most select London hotels."
"How did you deduce the select?"
"By the select prices. Eight shillings for a bed and eightpence
for a glass of sherry pointed to one of the most expensive
hotels. There are not many in London which charge at that rate.
In the second one which I visited in Northumberland Avenue, I
learned by an inspection of the book that Francis H. Moulton, an
American gentleman, had left only the day before, and on looking
over the entries against him, I came upon the very items which I
had seen in the duplicate bill. His letters were to be forwarded
to 226 Gordon Square; so thither I travelled, and being fortunate
enough to find the loving couple at home, I ventured to give them
some paternal advice and to point out to them that it would be
better in every way that they should make their position a little
clearer both to the general public and to Lord St. Simon in
particular. I invited them to meet him here, and, as you see, I
made him keep the appointment."
"But with no very good result," I remarked. "His conduct was
certainly not very gracious."
"Ah, Watson," said Holmes, smiling, "perhaps you would not be
very gracious either, if, after all the trouble of wooing and
wedding, you found yourself deprived in an instant of wife and of
fortune. I think that we may judge Lord St. Simon very mercifully
and thank our stars that we are never likely to find ourselves in
the same position. Draw your chair up and hand me my violin, for
the only problem we have still to solve is how to while away
these bleak autumnal evenings."
XI. THE ADVENTURE OF THE BERYL CORONET
"Holmes," said I as I stood one morning in our bow-window looking
down the street, "here is a madman coming along. It seems rather
sad that his relatives should allow him to come out alone."
My friend rose lazily from his armchair and stood with his hands
in the pockets of his dressing-gown, looking over my shoulder. It
was a bright, crisp February morning, and the snow of the day
before still lay deep upon the ground, shimmering brightly in the
wintry sun. Down the centre of Baker Street it had been ploughed
into a brown crumbly band by the traffic, but at either side and
on the heaped-up edges of the foot-paths it still lay as white as
when it fell. The grey pavement had been cleaned and scraped, but
was still dangerously slippery, so that there were fewer
passengers than usual. Indeed, from the direction of the
Metropolitan Station no one was coming save the single gentleman
whose eccentric conduct had drawn my attention.
He was a man of about fifty, tall, portly, and imposing, with a
massive, strongly marked face and a commanding figure. He was
dressed in a sombre yet rich style, in black frock-coat, shining
hat, neat brown gaiters, and well-cut pearl-grey trousers. Yet
his actions were in absurd contrast to the dignity of his dress
and features, for he was running hard, with occasional little
springs, such as a weary man gives who is little accustomed to
set any tax upon his legs. As he ran he jerked his hands up and
down, waggled his head, and writhed his face into the most
extraordinary contortions.
"What on earth can be the matter with him?" I asked. "He is
looking up at the numbers of the houses."
"I believe that he is coming here," said Holmes, rubbing his
hands.
"Here?"
"Yes; I rather think he is coming to consult me professionally. I
think that I recognise the symptoms. Ha! did I not tell you?" As
he spoke, the man, puffing and blowing, rushed at our door and
pulled at our bell until the whole house resounded with the
clanging.
A few moments later he was in our room, still puffing, still
gesticulating, but with so fixed a look of grief and despair in
his eyes that our smiles were turned in an instant to horror and
pity. For a while he could not get his words out, but swayed his
body and plucked at his hair like one who has been driven to the
extreme limits of his reason. Then, suddenly springing to his
feet, he beat his head against the wall with such force that we
both rushed upon him and tore him away to the centre of the room.
Sherlock Holmes pushed him down into the easy-chair and, sitting
beside him, patted his hand and chatted with him in the easy,
soothing tones which he knew so well how to employ.
"You have come to me to tell your story, have you not?" said he.
"You are fatigued with your haste. Pray wait until you have
recovered yourself, and then I shall be most happy to look into
any little problem which you may submit to me."
The man sat for a minute or more with a heaving chest, fighting
against his emotion. Then he passed his handkerchief over his
brow, set his lips tight, and turned his face towards us.
"No doubt you think me mad?" said he.
"I see that you have had some great trouble," responded Holmes.
"God knows I have!--a trouble which is enough to unseat my
reason, so sudden and so terrible is it. Public disgrace I might
have faced, although I am a man whose character has never yet
borne a stain. Private affliction also is the lot of every man;
but the two coming together, and in so frightful a form, have
been enough to shake my very soul. Besides, it is not I alone.
The very noblest in the land may suffer unless some way be found
out of this horrible affair."
"Pray compose yourself, sir," said Holmes, "and let me have a
clear account of who you are and what it is that has befallen
you."
"My name," answered our visitor, "is probably familiar to your
ears. I am Alexander Holder, of the banking firm of Holder &
Stevenson, of Threadneedle Street."
The name was indeed well known to us as belonging to the senior
partner in the second largest private banking concern in the City
of London. What could have happened, then, to bring one of the
foremost citizens of London to this most pitiable pass? We
waited, all curiosity, until with another effort he braced
himself to tell his story.
"I feel that time is of value," said he; "that is why I hastened
here when the police inspector suggested that I should secure
your co-operation. I came to Baker Street by the Underground and
hurried from there on foot, for the cabs go slowly through this
snow. That is why I was so out of breath, for I am a man who
takes very little exercise. I feel better now, and I will put the
facts before you as shortly and yet as clearly as I can.
"It is, of course, well known to you that in a successful banking
business as much depends upon our being able to find remunerative
investments for our funds as upon our increasing our connection
and the number of our depositors. One of our most lucrative means
of laying out money is in the shape of loans, where the security
is unimpeachable. We have done a good deal in this direction
during the last few years, and there are many noble families to
whom we have advanced large sums upon the security of their
pictures, libraries, or plate.
"Yesterday morning I was seated in my office at the bank when a
card was brought in to me by one of the clerks. I started when I
saw the name, for it was that of none other than--well, perhaps
even to you I had better say no more than that it was a name
which is a household word all over the earth--one of the highest,
noblest, most exalted names in England. I was overwhelmed by the
honour and attempted, when he entered, to say so, but he plunged
at once into business with the air of a man who wishes to hurry
quickly through a disagreeable task.
"'Mr. Holder,' said he, 'I have been informed that you are in the
habit of advancing money.'
"'The firm does so when the security is good.' I answered.
"'It is absolutely essential to me,' said he, 'that I should have
50,000 pounds at once. I could, of course, borrow so trifling a
sum ten times over from my friends, but I much prefer to make it
a matter of business and to carry out that business myself. In my
position you can readily understand that it is unwise to place
one's self under obligations.'
"'For how long, may I ask, do you want this sum?' I asked.
"'Next Monday I have a large sum due to me, and I shall then most
certainly repay what you advance, with whatever interest you
think it right to charge. But it is very essential to me that the
money should be paid at once.'
"'I should be happy to advance it without further parley from my
own private purse,' said I, 'were it not that the strain would be
rather more than it could bear. If, on the other hand, I am to do
it in the name of the firm, then in justice to my partner I must
insist that, even in your case, every businesslike precaution
should be taken.'
"'I should much prefer to have it so,' said he, raising up a
square, black morocco case which he had laid beside his chair.
'You have doubtless heard of the Beryl Coronet?'
"'One of the most precious public possessions of the empire,'
said I.
"'Precisely.' He opened the case, and there, imbedded in soft,
flesh-coloured velvet, lay the magnificent piece of jewellery
which he had named. 'There are thirty-nine enormous beryls,' said
he, 'and the price of the gold chasing is incalculable. The
lowest estimate would put the worth of the coronet at double the
sum which I have asked. I am prepared to leave it with you as my
security.'
"I took the precious case into my hands and looked in some
perplexity from it to my illustrious client.
"'You doubt its value?' he asked.
"'Not at all. I only doubt--'
"'The propriety of my leaving it. You may set your mind at rest
about that. I should not dream of doing so were it not absolutely
certain that I should be able in four days to reclaim it. It is a
pure matter of form. Is the security sufficient?'
"'Ample.'
"'You understand, Mr. Holder, that I am giving you a strong proof
of the confidence which I have in you, founded upon all that I
have heard of you. I rely upon you not only to be discreet and to
refrain from all gossip upon the matter but, above all, to
preserve this coronet with every possible precaution because I
need not say that a great public scandal would be caused if any
harm were to befall it. Any injury to it would be almost as
serious as its complete loss, for there are no beryls in the
world to match these, and it would be impossible to replace them.
I leave it with you, however, with every confidence, and I shall
call for it in person on Monday morning.'
"Seeing that my client was anxious to leave, I said no more but,
calling for my cashier, I ordered him to pay over fifty 1000
pound notes. When I was alone once more, however, with the
precious case lying upon the table in front of me, I could not
but think with some misgivings of the immense responsibility
which it entailed upon me. There could be no doubt that, as it
was a national possession, a horrible scandal would ensue if any
misfortune should occur to it. I already regretted having ever
consented to take charge of it. However, it was too late to alter
the matter now, so I locked it up in my private safe and turned
once more to my work.
"When evening came I felt that it would be an imprudence to leave
so precious a thing in the office behind me. Bankers' safes had
been forced before now, and why should not mine be? If so, how
terrible would be the position in which I should find myself! I
determined, therefore, that for the next few days I would always
carry the case backward and forward with me, so that it might
never be really out of my reach. With this intention, I called a
cab and drove out to my house at Streatham, carrying the jewel
with me. I did not breathe freely until I had taken it upstairs
and locked it in the bureau of my dressing-room.
"And now a word as to my household, Mr. Holmes, for I wish you to
thoroughly understand the situation. My groom and my page sleep
out of the house, and may be set aside altogether. I have three
maid-servants who have been with me a number of years and whose
absolute reliability is quite above suspicion. Another, Lucy
Parr, the second waiting-maid, has only been in my service a few
months. She came with an excellent character, however, and has
always given me satisfaction. She is a very pretty girl and has
attracted admirers who have occasionally hung about the place.
That is the only drawback which we have found to her, but we
believe her to be a thoroughly good girl in every way.
"So much for the servants. My family itself is so small that it
will not take me long to describe it. I am a widower and have an
only son, Arthur. He has been a disappointment to me, Mr.
Holmes--a grievous disappointment. I have no doubt that I am
myself to blame. People tell me that I have spoiled him. Very
likely I have. When my dear wife died I felt that he was all I
had to love. I could not bear to see the smile fade even for a
moment from his face. I have never denied him a wish. Perhaps it
would have been better for both of us had I been sterner, but I
meant it for the best.
"It was naturally my intention that he should succeed me in my
business, but he was not of a business turn. He was wild,
wayward, and, to speak the truth, I could not trust him in the
handling of large sums of money. When he was young he became a
member of an aristocratic club, and there, having charming
manners, he was soon the intimate of a number of men with long
purses and expensive habits. He learned to play heavily at cards
and to squander money on the turf, until he had again and again
to come to me and implore me to give him an advance upon his
allowance, that he might settle his debts of honour. He tried
more than once to break away from the dangerous company which he
was keeping, but each time the influence of his friend, Sir
George Burnwell, was enough to draw him back again.
"And, indeed, I could not wonder that such a man as Sir George
Burnwell should gain an influence over him, for he has frequently
brought him to my house, and I have found myself that I could
hardly resist the fascination of his manner. He is older than
Arthur, a man of the world to his finger-tips, one who had been
everywhere, seen everything, a brilliant talker, and a man of
great personal beauty. Yet when I think of him in cold blood, far
away from the glamour of his presence, I am convinced from his
cynical speech and the look which I have caught in his eyes that
he is one who should be deeply distrusted. So I think, and so,
too, thinks my little Mary, who has a woman's quick insight into
character.
"And now there is only she to be described. She is my niece; but
when my brother died five years ago and left her alone in the
world I adopted her, and have looked upon her ever since as my
daughter. She is a sunbeam in my house--sweet, loving, beautiful,
a wonderful manager and housekeeper, yet as tender and quiet and
gentle as a woman could be. She is my right hand. I do not know
what I could do without her. In only one matter has she ever gone
against my wishes. Twice my boy has asked her to marry him, for
he loves her devotedly, but each time she has refused him. I
think that if anyone could have drawn him into the right path it
would have been she, and that his marriage might have changed his
whole life; but now, alas! it is too late--forever too late!
"Now, Mr. Holmes, you know the people who live under my roof, and
I shall continue with my miserable story.
"When we were taking coffee in the drawing-room that night after
dinner, I told Arthur and Mary my experience, and of the precious
treasure which we had under our roof, suppressing only the name
of my client. Lucy Parr, who had brought in the coffee, had, I am
sure, left the room; but I cannot swear that the door was closed.
Mary and Arthur were much interested and wished to see the famous
coronet, but I thought it better not to disturb it.
"'Where have you put it?' asked Arthur.
"'In my own bureau.'
"'Well, I hope to goodness the house won't be burgled during the
night.' said he.
"'It is locked up,' I answered.
"'Oh, any old key will fit that bureau. When I was a youngster I
have opened it myself with the key of the box-room cupboard.'
"He often had a wild way of talking, so that I thought little of
what he said. He followed me to my room, however, that night with
a very grave face.
"'Look here, dad,' said he with his eyes cast down, 'can you let
me have 200 pounds?'
"'No, I cannot!' I answered sharply. 'I have been far too
generous with you in money matters.'
"'You have been very kind,' said he, 'but I must have this money,
or else I can never show my face inside the club again.'
"'And a very good thing, too!' I cried.
"'Yes, but you would not have me leave it a dishonoured man,'
said he. 'I could not bear the disgrace. I must raise the money
in some way, and if you will not let me have it, then I must try
other means.'
"I was very angry, for this was the third demand during the
month. 'You shall not have a farthing from me,' I cried, on which
he bowed and left the room without another word.
"When he was gone I unlocked my bureau, made sure that my
treasure was safe, and locked it again. Then I started to go
round the house to see that all was secure--a duty which I
usually leave to Mary but which I thought it well to perform
myself that night. As I came down the stairs I saw Mary herself
at the side window of the hall, which she closed and fastened as
I approached.
"'Tell me, dad,' said she, looking, I thought, a little
disturbed, 'did you give Lucy, the maid, leave to go out
to-night?'
"'Certainly not.'
"'She came in just now by the back door. I have no doubt that she
has only been to the side gate to see someone, but I think that
it is hardly safe and should be stopped.'
"'You must speak to her in the morning, or I will if you prefer
it. Are you sure that everything is fastened?'
"'Quite sure, dad.'
"'Then, good-night.' I kissed her and went up to my bedroom
again, where I was soon asleep.
"I am endeavouring to tell you everything, Mr. Holmes, which may
have any bearing upon the case, but I beg that you will question
me upon any point which I do not make clear."
"On the contrary, your statement is singularly lucid."
"I come to a part of my story now in which I should wish to be
particularly so. I am not a very heavy sleeper, and the anxiety
in my mind tended, no doubt, to make me even less so than usual.
About two in the morning, then, I was awakened by some sound in
the house. It had ceased ere I was wide awake, but it had left an
impression behind it as though a window had gently closed
somewhere. I lay listening with all my ears. Suddenly, to my
horror, there was a distinct sound of footsteps moving softly in
the next room. I slipped out of bed, all palpitating with fear,
and peeped round the corner of my dressing-room door.
"'Arthur!' I screamed, 'you villain! you thief! How dare you
touch that coronet?'
"The gas was half up, as I had left it, and my unhappy boy,
dressed only in his shirt and trousers, was standing beside the
light, holding the coronet in his hands. He appeared to be
wrenching at it, or bending it with all his strength. At my cry
he dropped it from his grasp and turned as pale as death. I
snatched it up and examined it. One of the gold corners, with
three of the beryls in it, was missing.
"'You blackguard!' I shouted, beside myself with rage. 'You have
destroyed it! You have dishonoured me forever! Where are the
jewels which you have stolen?'
"'Stolen!' he cried.
"'Yes, thief!' I roared, shaking him by the shoulder.
"'There are none missing. There cannot be any missing,' said he.
"'There are three missing. And you know where they are. Must I
call you a liar as well as a thief? Did I not see you trying to
tear off another piece?'
"'You have called me names enough,' said he, 'I will not stand it
any longer. I shall not say another word about this business,
since you have chosen to insult me. I will leave your house in
the morning and make my own way in the world.'
"'You shall leave it in the hands of the police!' I cried
half-mad with grief and rage. 'I shall have this matter probed to
the bottom.'
"'You shall learn nothing from me,' said he with a passion such
as I should not have thought was in his nature. 'If you choose to
call the police, let the police find what they can.'
"By this time the whole house was astir, for I had raised my
voice in my anger. Mary was the first to rush into my room, and,
at the sight of the coronet and of Arthur's face, she read the
whole story and, with a scream, fell down senseless on the
ground. I sent the house-maid for the police and put the
investigation into their hands at once. When the inspector and a
constable entered the house, Arthur, who had stood sullenly with
his arms folded, asked me whether it was my intention to charge
him with theft. I answered that it had ceased to be a private
matter, but had become a public one, since the ruined coronet was
national property. I was determined that the law should have its
way in everything.
"'At least,' said he, 'you will not have me arrested at once. It
would be to your advantage as well as mine if I might leave the
house for five minutes.'
"'That you may get away, or perhaps that you may conceal what you
have stolen,' said I. And then, realising the dreadful position
in which I was placed, I implored him to remember that not only
my honour but that of one who was far greater than I was at
stake; and that he threatened to raise a scandal which would
convulse the nation. He might avert it all if he would but tell
me what he had done with the three missing stones.
"'You may as well face the matter,' said I; 'you have been caught
in the act, and no confession could make your guilt more heinous.
If you but make such reparation as is in your power, by telling
us where the beryls are, all shall be forgiven and forgotten.'
"'Keep your forgiveness for those who ask for it,' he answered,
turning away from me with a sneer. I saw that he was too hardened
for any words of mine to influence him. There was but one way for
it. I called in the inspector and gave him into custody. A search
was made at once not only of his person but of his room and of
every portion of the house where he could possibly have concealed
the gems; but no trace of them could be found, nor would the
wretched boy open his mouth for all our persuasions and our
threats. This morning he was removed to a cell, and I, after
going through all the police formalities, have hurried round to
you to implore you to use your skill in unravelling the matter.
The police have openly confessed that they can at present make
nothing of it. You may go to any expense which you think
necessary. I have already offered a reward of 1000 pounds. My
God, what shall I do! I have lost my honour, my gems, and my son
in one night. Oh, what shall I do!"
He put a hand on either side of his head and rocked himself to
and fro, droning to himself like a child whose grief has got
beyond words.
Sherlock Holmes sat silent for some few minutes, with his brows
knitted and his eyes fixed upon the fire.
"Do you receive much company?" he asked.
"None save my partner with his family and an occasional friend of
Arthur's. Sir George Burnwell has been several times lately. No
one else, I think."
"Do you go out much in society?"
"Arthur does. Mary and I stay at home. We neither of us care for
it."
"That is unusual in a young girl."
"She is of a quiet nature. Besides, she is not so very young. She
is four-and-twenty."
"This matter, from what you say, seems to have been a shock to
her also."
"Terrible! She is even more affected than I."
"You have neither of you any doubt as to your son's guilt?"
"How can we have when I saw him with my own eyes with the coronet
in his hands."
"I hardly consider that a conclusive proof. Was the remainder of
the coronet at all injured?"
"Yes, it was twisted."
"Do you not think, then, that he might have been trying to
straighten it?"
"God bless you! You are doing what you can for him and for me.
But it is too heavy a task. What was he doing there at all? If
his purpose were innocent, why did he not say so?"
"Precisely. And if it were guilty, why did he not invent a lie?
His silence appears to me to cut both ways. There are several
singular points about the case. What did the police think of the
noise which awoke you from your sleep?"
"They considered that it might be caused by Arthur's closing his
bedroom door."
"A likely story! As if a man bent on felony would slam his door
so as to wake a household. What did they say, then, of the
disappearance of these gems?"
"They are still sounding the planking and probing the furniture
in the hope of finding them."
"Have they thought of looking outside the house?"
"Yes, they have shown extraordinary energy. The whole garden has
already been minutely examined."
"Now, my dear sir," said Holmes, "is it not obvious to you now
that this matter really strikes very much deeper than either you
or the police were at first inclined to think? It appeared to you
to be a simple case; to me it seems exceedingly complex. Consider
what is involved by your theory. You suppose that your son came
down from his bed, went, at great risk, to your dressing-room,
opened your bureau, took out your coronet, broke off by main
force a small portion of it, went off to some other place,
concealed three gems out of the thirty-nine, with such skill that
nobody can find them, and then returned with the other thirty-six
into the room in which he exposed himself to the greatest danger
of being discovered. I ask you now, is such a theory tenable?"
"But what other is there?" cried the banker with a gesture of
despair. "If his motives were innocent, why does he not explain
them?"
"It is our task to find that out," replied Holmes; "so now, if
you please, Mr. Holder, we will set off for Streatham together,
and devote an hour to glancing a little more closely into
details."
My friend insisted upon my accompanying them in their expedition,
which I was eager enough to do, for my curiosity and sympathy
were deeply stirred by the story to which we had listened. I
confess that the guilt of the banker's son appeared to me to be
as obvious as it did to his unhappy father, but still I had such
faith in Holmes' judgment that I felt that there must be some
grounds for hope as long as he was dissatisfied with the accepted
explanation. He hardly spoke a word the whole way out to the
southern suburb, but sat with his chin upon his breast and his
hat drawn over his eyes, sunk in the deepest thought. Our client
appeared to have taken fresh heart at the little glimpse of hope
which had been presented to him, and he even broke into a
desultory chat with me over his business affairs. A short railway
journey and a shorter walk brought us to Fairbank, the modest
residence of the great financier.
Fairbank was a good-sized square house of white stone, standing
back a little from the road. A double carriage-sweep, with a
snow-clad lawn, stretched down in front to two large iron gates
which closed the entrance. On the right side was a small wooden
thicket, which led into a narrow path between two neat hedges
stretching from the road to the kitchen door, and forming the
tradesmen's entrance. On the left ran a lane which led to the
stables, and was not itself within the grounds at all, being a
public, though little used, thoroughfare. Holmes left us standing
at the door and walked slowly all round the house, across the
front, down the tradesmen's path, and so round by the garden
behind into the stable lane. So long was he that Mr. Holder and I
went into the dining-room and waited by the fire until he should
return. We were sitting there in silence when the door opened and
a young lady came in. She was rather above the middle height,
slim, with dark hair and eyes, which seemed the darker against
the absolute pallor of her skin. I do not think that I have ever
seen such deadly paleness in a woman's face. Her lips, too, were
bloodless, but her eyes were flushed with crying. As she swept
silently into the room she impressed me with a greater sense of
grief than the banker had done in the morning, and it was the
more striking in her as she was evidently a woman of strong
character, with immense capacity for self-restraint. Disregarding
my presence, she went straight to her uncle and passed her hand
over his head with a sweet womanly caress.
"You have given orders that Arthur should be liberated, have you
not, dad?" she asked.
"No, no, my girl, the matter must be probed to the bottom."
"But I am so sure that he is innocent. You know what woman's
instincts are. I know that he has done no harm and that you will
be sorry for having acted so harshly."
"Why is he silent, then, if he is innocent?"
"Who knows? Perhaps because he was so angry that you should
suspect him."
"How could I help suspecting him, when I actually saw him with
the coronet in his hand?"
"Oh, but he had only picked it up to look at it. Oh, do, do take
my word for it that he is innocent. Let the matter drop and say
no more. It is so dreadful to think of our dear Arthur in
prison!"
"I shall never let it drop until the gems are found--never, Mary!
Your affection for Arthur blinds you as to the awful consequences
to me. Far from hushing the thing up, I have brought a gentleman
down from London to inquire more deeply into it."
"This gentleman?" she asked, facing round to me.
"No, his friend. He wished us to leave him alone. He is round in
the stable lane now."
"The stable lane?" She raised her dark eyebrows. "What can he
hope to find there? Ah! this, I suppose, is he. I trust, sir,
that you will succeed in proving, what I feel sure is the truth,
that my cousin Arthur is innocent of this crime."
"I fully share your opinion, and I trust, with you, that we may
prove it," returned Holmes, going back to the mat to knock the
snow from his shoes. "I believe I have the honour of addressing
Miss Mary Holder. Might I ask you a question or two?"
"Pray do, sir, if it may help to clear this horrible affair up."
"You heard nothing yourself last night?"
"Nothing, until my uncle here began to speak loudly. I heard
that, and I came down."
"You shut up the windows and doors the night before. Did you
fasten all the windows?"
"Yes."
"Were they all fastened this morning?"
"Yes."
"You have a maid who has a sweetheart? I think that you remarked
to your uncle last night that she had been out to see him?"
"Yes, and she was the girl who waited in the drawing-room, and
who may have heard uncle's remarks about the coronet."
"I see. You infer that she may have gone out to tell her
sweetheart, and that the two may have planned the robbery."
"But what is the good of all these vague theories," cried the
banker impatiently, "when I have told you that I saw Arthur with
the coronet in his hands?"
"Wait a little, Mr. Holder. We must come back to that. About this
girl, Miss Holder. You saw her return by the kitchen door, I
presume?"
"Yes; when I went to see if the door was fastened for the night I
met her slipping in. I saw the man, too, in the gloom."
"Do you know him?"
"Oh, yes! he is the green-grocer who brings our vegetables round.
His name is Francis Prosper."
"He stood," said Holmes, "to the left of the door--that is to
say, farther up the path than is necessary to reach the door?"
"Yes, he did."
"And he is a man with a wooden leg?"
Something like fear sprang up in the young lady's expressive
black eyes. "Why, you are like a magician," said she. "How do you
know that?" She smiled, but there was no answering smile in
Holmes' thin, eager face.
"I should be very glad now to go upstairs," said he. "I shall
probably wish to go over the outside of the house again. Perhaps
I had better take a look at the lower windows before I go up."
He walked swiftly round from one to the other, pausing only at
the large one which looked from the hall onto the stable lane.
This he opened and made a very careful examination of the sill
with his powerful magnifying lens. "Now we shall go upstairs,"
said he at last.
The banker's dressing-room was a plainly furnished little
chamber, with a grey carpet, a large bureau, and a long mirror.
Holmes went to the bureau first and looked hard at the lock.
"Which key was used to open it?" he asked.
"That which my son himself indicated--that of the cupboard of the
lumber-room."
"Have you it here?"
"That is it on the dressing-table."
Sherlock Holmes took it up and opened the bureau.
"It is a noiseless lock," said he. "It is no wonder that it did
not wake you. This case, I presume, contains the coronet. We must
have a look at it." He opened the case, and taking out the diadem
he laid it upon the table. It was a magnificent specimen of the
jeweller's art, and the thirty-six stones were the finest that I
have ever seen. At one side of the coronet was a cracked edge,
where a corner holding three gems had been torn away.
"Now, Mr. Holder," said Holmes, "here is the corner which
corresponds to that which has been so unfortunately lost. Might I
beg that you will break it off."
The banker recoiled in horror. "I should not dream of trying,"
said he.
"Then I will." Holmes suddenly bent his strength upon it, but
without result. "I feel it give a little," said he; "but, though
I am exceptionally strong in the fingers, it would take me all my
time to break it. An ordinary man could not do it. Now, what do
you think would happen if I did break it, Mr. Holder? There would
be a noise like a pistol shot. Do you tell me that all this
happened within a few yards of your bed and that you heard
nothing of it?"
"I do not know what to think. It is all dark to me."
"But perhaps it may grow lighter as we go. What do you think,
Miss Holder?"
"I confess that I still share my uncle's perplexity."
"Your son had no shoes or slippers on when you saw him?"
"He had nothing on save only his trousers and shirt."
"Thank you. We have certainly been favoured with extraordinary
luck during this inquiry, and it will be entirely our own fault
if we do not succeed in clearing the matter up. With your
permission, Mr. Holder, I shall now continue my investigations
outside."
He went alone, at his own request, for he explained that any
unnecessary footmarks might make his task more difficult. For an
hour or more he was at work, returning at last with his feet
heavy with snow and his features as inscrutable as ever.
"I think that I have seen now all that there is to see, Mr.
Holder," said he; "I can serve you best by returning to my
rooms."
"But the gems, Mr. Holmes. Where are they?"
"I cannot tell."
The banker wrung his hands. "I shall never see them again!" he
cried. "And my son? You give me hopes?"
"My opinion is in no way altered."
"Then, for God's sake, what was this dark business which was
acted in my house last night?"
"If you can call upon me at my Baker Street rooms to-morrow
morning between nine and ten I shall be happy to do what I can to
make it clearer. I understand that you give me carte blanche to
act for you, provided only that I get back the gems, and that you
place no limit on the sum I may draw."
"I would give my fortune to have them back."
"Very good. I shall look into the matter between this and then.
Good-bye; it is just possible that I may have to come over here
again before evening."
It was obvious to me that my companion's mind was now made up
about the case, although what his conclusions were was more than
I could even dimly imagine. Several times during our homeward
journey I endeavoured to sound him upon the point, but he always
glided away to some other topic, until at last I gave it over in
despair. It was not yet three when we found ourselves in our
rooms once more. He hurried to his chamber and was down again in
a few minutes dressed as a common loafer. With his collar turned
up, his shiny, seedy coat, his red cravat, and his worn boots, he
was a perfect sample of the class.
"I think that this should do," said he, glancing into the glass
above the fireplace. "I only wish that you could come with me,
Watson, but I fear that it won't do. I may be on the trail in
this matter, or I may be following a will-o'-the-wisp, but I
shall soon know which it is. I hope that I may be back in a few
hours." He cut a slice of beef from the joint upon the sideboard,
sandwiched it between two rounds of bread, and thrusting this
rude meal into his pocket he started off upon his expedition.
I had just finished my tea when he returned, evidently in
excellent spirits, swinging an old elastic-sided boot in his
hand. He chucked it down into a corner and helped himself to a
cup of tea.
"I only looked in as I passed," said he. "I am going right on."
"Where to?"
"Oh, to the other side of the West End. It may be some time
before I get back. Don't wait up for me in case I should be
late."
"How are you getting on?"
"Oh, so so. Nothing to complain of. I have been out to Streatham
since I saw you last, but I did not call at the house. It is a
very sweet little problem, and I would not have missed it for a
good deal. However, I must not sit gossiping here, but must get
these disreputable clothes off and return to my highly
respectable self."
I could see by his manner that he had stronger reasons for
satisfaction than his words alone would imply. His eyes twinkled,
and there was even a touch of colour upon his sallow cheeks. He
hastened upstairs, and a few minutes later I heard the slam of
the hall door, which told me that he was off once more upon his
congenial hunt.
I waited until midnight, but there was no sign of his return, so
I retired to my room. It was no uncommon thing for him to be away
for days and nights on end when he was hot upon a scent, so that
his lateness caused me no surprise. I do not know at what hour he
came in, but when I came down to breakfast in the morning there
he was with a cup of coffee in one hand and the paper in the
other, as fresh and trim as possible.
"You will excuse my beginning without you, Watson," said he, "but
you remember that our client has rather an early appointment this
morning."
"Why, it is after nine now," I answered. "I should not be
surprised if that were he. I thought I heard a ring."
It was, indeed, our friend the financier. I was shocked by the
change which had come over him, for his face which was naturally
of a broad and massive mould, was now pinched and fallen in,
while his hair seemed to me at least a shade whiter. He entered
with a weariness and lethargy which was even more painful than
his violence of the morning before, and he dropped heavily into
the armchair which I pushed forward for him.
"I do not know what I have done to be so severely tried," said
he. "Only two days ago I was a happy and prosperous man, without
a care in the world. Now I am left to a lonely and dishonoured
age. One sorrow comes close upon the heels of another. My niece,
Mary, has deserted me."
"Deserted you?"
"Yes. Her bed this morning had not been slept in, her room was
empty, and a note for me lay upon the hall table. I had said to
her last night, in sorrow and not in anger, that if she had
married my boy all might have been well with him. Perhaps it was
thoughtless of me to say so. It is to that remark that she refers
in this note:
"'MY DEAREST UNCLE:--I feel that I have brought trouble upon you,
and that if I had acted differently this terrible misfortune
might never have occurred. I cannot, with this thought in my
mind, ever again be happy under your roof, and I feel that I must
leave you forever. Do not worry about my future, for that is
provided for; and, above all, do not search for me, for it will
be fruitless labour and an ill-service to me. In life or in
death, I am ever your loving,--MARY.'
"What could she mean by that note, Mr. Holmes? Do you think it
points to suicide?"
"No, no, nothing of the kind. It is perhaps the best possible
solution. I trust, Mr. Holder, that you are nearing the end of
your troubles."
"Ha! You say so! You have heard something, Mr. Holmes; you have
learned something! Where are the gems?"
"You would not think 1000 pounds apiece an excessive sum for
them?"
"I would pay ten."
"That would be unnecessary. Three thousand will cover the matter.
And there is a little reward, I fancy. Have you your check-book?
Here is a pen. Better make it out for 4000 pounds."
With a dazed face the banker made out the required check. Holmes
walked over to his desk, took out a little triangular piece of
gold with three gems in it, and threw it down upon the table.
With a shriek of joy our client clutched it up.
"You have it!" he gasped. "I am saved! I am saved!"
The reaction of joy was as passionate as his grief had been, and
he hugged his recovered gems to his bosom.
"There is one other thing you owe, Mr. Holder," said Sherlock
Holmes rather sternly.
"Owe!" He caught up a pen. "Name the sum, and I will pay it."
"No, the debt is not to me. You owe a very humble apology to that
noble lad, your son, who has carried himself in this matter as I
should be proud to see my own son do, should I ever chance to
have one."
"Then it was not Arthur who took them?"
"I told you yesterday, and I repeat to-day, that it was not."
"You are sure of it! Then let us hurry to him at once to let him
know that the truth is known."
"He knows it already. When I had cleared it all up I had an
interview with him, and finding that he would not tell me the
story, I told it to him, on which he had to confess that I was
right and to add the very few details which were not yet quite
clear to me. Your news of this morning, however, may open his
lips."
"For heaven's sake, tell me, then, what is this extraordinary
mystery!"
"I will do so, and I will show you the steps by which I reached
it. And let me say to you, first, that which it is hardest for me
to say and for you to hear: there has been an understanding
between Sir George Burnwell and your niece Mary. They have now
fled together."
"My Mary? Impossible!"
"It is unfortunately more than possible; it is certain. Neither
you nor your son knew the true character of this man when you
admitted him into your family circle. He is one of the most
dangerous men in England--a ruined gambler, an absolutely
desperate villain, a man without heart or conscience. Your niece
knew nothing of such men. When he breathed his vows to her, as he
had done to a hundred before her, she flattered herself that she
alone had touched his heart. The devil knows best what he said,
but at least she became his tool and was in the habit of seeing
him nearly every evening."
"I cannot, and I will not, believe it!" cried the banker with an
ashen face.
"I will tell you, then, what occurred in your house last night.
Your niece, when you had, as she thought, gone to your room,
slipped down and talked to her lover through the window which
leads into the stable lane. His footmarks had pressed right
through the snow, so long had he stood there. She told him of the
coronet. His wicked lust for gold kindled at the news, and he
bent her to his will. I have no doubt that she loved you, but
there are women in whom the love of a lover extinguishes all
other loves, and I think that she must have been one. She had
hardly listened to his instructions when she saw you coming
downstairs, on which she closed the window rapidly and told you
about one of the servants' escapade with her wooden-legged lover,
which was all perfectly true.
"Your boy, Arthur, went to bed after his interview with you but
he slept badly on account of his uneasiness about his club debts.
In the middle of the night he heard a soft tread pass his door,
so he rose and, looking out, was surprised to see his cousin
walking very stealthily along the passage until she disappeared
into your dressing-room. Petrified with astonishment, the lad
slipped on some clothes and waited there in the dark to see what
would come of this strange affair. Presently she emerged from the
room again, and in the light of the passage-lamp your son saw
that she carried the precious coronet in her hands. She passed
down the stairs, and he, thrilling with horror, ran along and
slipped behind the curtain near your door, whence he could see
what passed in the hall beneath. He saw her stealthily open the
window, hand out the coronet to someone in the gloom, and then
closing it once more hurry back to her room, passing quite close
to where he stood hid behind the curtain.
"As long as she was on the scene he could not take any action
without a horrible exposure of the woman whom he loved. But the
instant that she was gone he realised how crushing a misfortune
this would be for you, and how all-important it was to set it
right. He rushed down, just as he was, in his bare feet, opened
the window, sprang out into the snow, and ran down the lane,
where he could see a dark figure in the moonlight. Sir George
Burnwell tried to get away, but Arthur caught him, and there was
a struggle between them, your lad tugging at one side of the
coronet, and his opponent at the other. In the scuffle, your son
struck Sir George and cut him over the eye. Then something
suddenly snapped, and your son, finding that he had the coronet
in his hands, rushed back, closed the window, ascended to your
room, and had just observed that the coronet had been twisted in
the struggle and was endeavouring to straighten it when you
appeared upon the scene."
"Is it possible?" gasped the banker.
"You then roused his anger by calling him names at a moment when
he felt that he had deserved your warmest thanks. He could not
explain the true state of affairs without betraying one who
certainly deserved little enough consideration at his hands. He
took the more chivalrous view, however, and preserved her
secret."
"And that was why she shrieked and fainted when she saw the
coronet," cried Mr. Holder. "Oh, my God! what a blind fool I have
been! And his asking to be allowed to go out for five minutes!
The dear fellow wanted to see if the missing piece were at the
scene of the struggle. How cruelly I have misjudged him!"
"When I arrived at the house," continued Holmes, "I at once went
very carefully round it to observe if there were any traces in
the snow which might help me. I knew that none had fallen since
the evening before, and also that there had been a strong frost
to preserve impressions. I passed along the tradesmen's path, but
found it all trampled down and indistinguishable. Just beyond it,
however, at the far side of the kitchen door, a woman had stood
and talked with a man, whose round impressions on one side showed
that he had a wooden leg. I could even tell that they had been
disturbed, for the woman had run back swiftly to the door, as was
shown by the deep toe and light heel marks, while Wooden-leg had
waited a little, and then had gone away. I thought at the time
that this might be the maid and her sweetheart, of whom you had
already spoken to me, and inquiry showed it was so. I passed
round the garden without seeing anything more than random tracks,
which I took to be the police; but when I got into the stable
lane a very long and complex story was written in the snow in
front of me.
"There was a double line of tracks of a booted man, and a second
double line which I saw with delight belonged to a man with naked
feet. I was at once convinced from what you had told me that the
latter was your son. The first had walked both ways, but the
other had run swiftly, and as his tread was marked in places over
the depression of the boot, it was obvious that he had passed
after the other. I followed them up and found they led to the
hall window, where Boots had worn all the snow away while
waiting. Then I walked to the other end, which was a hundred
yards or more down the lane. I saw where Boots had faced round,
where the snow was cut up as though there had been a struggle,
and, finally, where a few drops of blood had fallen, to show me
that I was not mistaken. Boots had then run down the lane, and
another little smudge of blood showed that it was he who had been
hurt. When he came to the highroad at the other end, I found that
the pavement had been cleared, so there was an end to that clue.
"On entering the house, however, I examined, as you remember, the
sill and framework of the hall window with my lens, and I could
at once see that someone had passed out. I could distinguish the
outline of an instep where the wet foot had been placed in coming
in. I was then beginning to be able to form an opinion as to what
had occurred. A man had waited outside the window; someone had
brought the gems; the deed had been overseen by your son; he had
pursued the thief; had struggled with him; they had each tugged
at the coronet, their united strength causing injuries which
neither alone could have effected. He had returned with the
prize, but had left a fragment in the grasp of his opponent. So
far I was clear. The question now was, who was the man and who
was it brought him the coronet?
"It is an old maxim of mine that when you have excluded the
impossible, whatever remains, however improbable, must be the
truth. Now, I knew that it was not you who had brought it down,
so there only remained your niece and the maids. But if it were
the maids, why should your son allow himself to be accused in
their place? There could be no possible reason. As he loved his
cousin, however, there was an excellent explanation why he should
retain her secret--the more so as the secret was a disgraceful
one. When I remembered that you had seen her at that window, and
how she had fainted on seeing the coronet again, my conjecture
became a certainty.
"And who could it be who was her confederate? A lover evidently,
for who else could outweigh the love and gratitude which she must
feel to you? I knew that you went out little, and that your
circle of friends was a very limited one. But among them was Sir
George Burnwell. I had heard of him before as being a man of evil
reputation among women. It must have been he who wore those boots
and retained the missing gems. Even though he knew that Arthur
had discovered him, he might still flatter himself that he was
safe, for the lad could not say a word without compromising his
own family.
"Well, your own good sense will suggest what measures I took
next. I went in the shape of a loafer to Sir George's house,
managed to pick up an acquaintance with his valet, learned that
his master had cut his head the night before, and, finally, at
the expense of six shillings, made all sure by buying a pair of
his cast-off shoes. With these I journeyed down to Streatham and
saw that they exactly fitted the tracks."
"I saw an ill-dressed vagabond in the lane yesterday evening,"
said Mr. Holder.
"Precisely. It was I. I found that I had my man, so I came home
and changed my clothes. It was a delicate part which I had to
play then, for I saw that a prosecution must be avoided to avert
scandal, and I knew that so astute a villain would see that our
hands were tied in the matter. I went and saw him. At first, of
course, he denied everything. But when I gave him every
particular that had occurred, he tried to bluster and took down a
life-preserver from the wall. I knew my man, however, and I
clapped a pistol to his head before he could strike. Then he
became a little more reasonable. I told him that we would give
him a price for the stones he held--1000 pounds apiece. That
brought out the first signs of grief that he had shown. 'Why,
dash it all!' said he, 'I've let them go at six hundred for the
three!' I soon managed to get the address of the receiver who had
them, on promising him that there would be no prosecution. Off I
set to him, and after much chaffering I got our stones at 1000
pounds apiece. Then I looked in upon your son, told him that all
was right, and eventually got to my bed about two o'clock, after
what I may call a really hard day's work."
"A day which has saved England from a great public scandal," said
the banker, rising. "Sir, I cannot find words to thank you, but
you shall not find me ungrateful for what you have done. Your
skill has indeed exceeded all that I have heard of it. And now I
must fly to my dear boy to apologise to him for the wrong which I
have done him. As to what you tell me of poor Mary, it goes to my
very heart. Not even your skill can inform me where she is now."
"I think that we may safely say," returned Holmes, "that she is
wherever Sir George Burnwell is. It is equally certain, too, that
whatever her sins are, they will soon receive a more than
sufficient punishment."
XII. THE ADVENTURE OF THE COPPER BEECHES
"To the man who loves art for its own sake," remarked Sherlock
Holmes, tossing aside the advertisement sheet of the Daily
Telegraph, "it is frequently in its least important and lowliest
manifestations that the keenest pleasure is to be derived. It is
pleasant to me to observe, Watson, that you have so far grasped
this truth that in these little records of our cases which you
have been good enough to draw up, and, I am bound to say,
occasionally to embellish, you have given prominence not so much
to the many causes célèbres and sensational trials in which I
have figured but rather to those incidents which may have been
trivial in themselves, but which have given room for those
faculties of deduction and of logical synthesis which I have made
my special province."
"And yet," said I, smiling, "I cannot quite hold myself absolved
from the charge of sensationalism which has been urged against my
records."
"You have erred, perhaps," he observed, taking up a glowing
cinder with the tongs and lighting with it the long cherry-wood
pipe which was wont to replace his clay when he was in a
disputatious rather than a meditative mood--"you have erred
perhaps in attempting to put colour and life into each of your
statements instead of confining yourself to the task of placing
upon record that severe reasoning from cause to effect which is
really the only notable feature about the thing."
"It seems to me that I have done you full justice in the matter,"
I remarked with some coldness, for I was repelled by the egotism
which I had more than once observed to be a strong factor in my
friend's singular character.
"No, it is not selfishness or conceit," said he, answering, as
was his wont, my thoughts rather than my words. "If I claim full
justice for my art, it is because it is an impersonal thing--a
thing beyond myself. Crime is common. Logic is rare. Therefore it
is upon the logic rather than upon the crime that you should
dwell. You have degraded what should have been a course of
lectures into a series of tales."
It was a cold morning of the early spring, and we sat after
breakfast on either side of a cheery fire in the old room at
Baker Street. A thick fog rolled down between the lines of
dun-coloured houses, and the opposing windows loomed like dark,
shapeless blurs through the heavy yellow wreaths. Our gas was lit
and shone on the white cloth and glimmer of china and metal, for
the table had not been cleared yet. Sherlock Holmes had been
silent all the morning, dipping continuously into the
advertisement columns of a succession of papers until at last,
having apparently given up his search, he had emerged in no very
sweet temper to lecture me upon my literary shortcomings.
"At the same time," he remarked after a pause, during which he
had sat puffing at his long pipe and gazing down into the fire,
"you can hardly be open to a charge of sensationalism, for out of
these cases which you have been so kind as to interest yourself
in, a fair proportion do not treat of crime, in its legal sense,
at all. The small matter in which I endeavoured to help the King
of Bohemia, the singular experience of Miss Mary Sutherland, the
problem connected with the man with the twisted lip, and the
incident of the noble bachelor, were all matters which are
outside the pale of the law. But in avoiding the sensational, I
fear that you may have bordered on the trivial."
"The end may have been so," I answered, "but the methods I hold
to have been novel and of interest."
"Pshaw, my dear fellow, what do the public, the great unobservant
public, who could hardly tell a weaver by his tooth or a
compositor by his left thumb, care about the finer shades of
analysis and deduction! But, indeed, if you are trivial, I cannot
blame you, for the days of the great cases are past. Man, or at
least criminal man, has lost all enterprise and originality. As
to my own little practice, it seems to be degenerating into an
agency for recovering lost lead pencils and giving advice to
young ladies from boarding-schools. I think that I have touched
bottom at last, however. This note I had this morning marks my
zero-point, I fancy. Read it!" He tossed a crumpled letter across
to me.
It was dated from Montague Place upon the preceding evening, and
ran thus:
"DEAR MR. HOLMES:--I am very anxious to consult you as to whether
I should or should not accept a situation which has been offered
to me as governess. I shall call at half-past ten to-morrow if I
do not inconvenience you. Yours faithfully,
"VIOLET HUNTER."
"Do you know the young lady?" I asked.
"Not I."
"It is half-past ten now."
"Yes, and I have no doubt that is her ring."
"It may turn out to be of more interest than you think. You
remember that the affair of the blue carbuncle, which appeared to
be a mere whim at first, developed into a serious investigation.
It may be so in this case, also."
"Well, let us hope so. But our doubts will very soon be solved,
for here, unless I am much mistaken, is the person in question."
As he spoke the door opened and a young lady entered the room.
She was plainly but neatly dressed, with a bright, quick face,
freckled like a plover's egg, and with the brisk manner of a
woman who has had her own way to make in the world.
"You will excuse my troubling you, I am sure," said she, as my
companion rose to greet her, "but I have had a very strange
experience, and as I have no parents or relations of any sort
from whom I could ask advice, I thought that perhaps you would be
kind enough to tell me what I should do."
"Pray take a seat, Miss Hunter. I shall be happy to do anything
that I can to serve you."
I could see that Holmes was favourably impressed by the manner
and speech of his new client. He looked her over in his searching
fashion, and then composed himself, with his lids drooping and
his finger-tips together, to listen to her story.
"I have been a governess for five years," said she, "in the
family of Colonel Spence Munro, but two months ago the colonel
received an appointment at Halifax, in Nova Scotia, and took his
children over to America with him, so that I found myself without
a situation. I advertised, and I answered advertisements, but
without success. At last the little money which I had saved began
to run short, and I was at my wit's end as to what I should do.
"There is a well-known agency for governesses in the West End
called Westaway's, and there I used to call about once a week in
order to see whether anything had turned up which might suit me.
Westaway was the name of the founder of the business, but it is
really managed by Miss Stoper. She sits in her own little office,
and the ladies who are seeking employment wait in an anteroom,
and are then shown in one by one, when she consults her ledgers
and sees whether she has anything which would suit them.
"Well, when I called last week I was shown into the little office
as usual, but I found that Miss Stoper was not alone. A
prodigiously stout man with a very smiling face and a great heavy
chin which rolled down in fold upon fold over his throat sat at
her elbow with a pair of glasses on his nose, looking very
earnestly at the ladies who entered. As I came in he gave quite a
jump in his chair and turned quickly to Miss Stoper.
"'That will do,' said he; 'I could not ask for anything better.
Capital! capital!' He seemed quite enthusiastic and rubbed his
hands together in the most genial fashion. He was such a
comfortable-looking man that it was quite a pleasure to look at
him.
"'You are looking for a situation, miss?' he asked.
"'Yes, sir.'
"'As governess?'
"'Yes, sir.'
"'And what salary do you ask?'
"'I had 4 pounds a month in my last place with Colonel Spence
Munro.'
"'Oh, tut, tut! sweating--rank sweating!' he cried, throwing his
fat hands out into the air like a man who is in a boiling
passion. 'How could anyone offer so pitiful a sum to a lady with
such attractions and accomplishments?'
"'My accomplishments, sir, may be less than you imagine,' said I.
'A little French, a little German, music, and drawing--'
"'Tut, tut!' he cried. 'This is all quite beside the question.
The point is, have you or have you not the bearing and deportment
of a lady? There it is in a nutshell. If you have not, you are
not fitted for the rearing of a child who may some day play a
considerable part in the history of the country. But if you have
why, then, how could any gentleman ask you to condescend to
accept anything under the three figures? Your salary with me,
madam, would commence at 100 pounds a year.'
"You may imagine, Mr. Holmes, that to me, destitute as I was,
such an offer seemed almost too good to be true. The gentleman,
however, seeing perhaps the look of incredulity upon my face,
opened a pocket-book and took out a note.
"'It is also my custom,' said he, smiling in the most pleasant
fashion until his eyes were just two little shining slits amid
the white creases of his face, 'to advance to my young ladies
half their salary beforehand, so that they may meet any little
expenses of their journey and their wardrobe.'
"It seemed to me that I had never met so fascinating and so
thoughtful a man. As I was already in debt to my tradesmen, the
advance was a great convenience, and yet there was something
unnatural about the whole transaction which made me wish to know
a little more before I quite committed myself.
"'May I ask where you live, sir?' said I.
"'Hampshire. Charming rural place. The Copper Beeches, five miles
on the far side of Winchester. It is the most lovely country, my
dear young lady, and the dearest old country-house.'
"'And my duties, sir? I should be glad to know what they would
be.'
"'One child--one dear little romper just six years old. Oh, if
you could see him killing cockroaches with a slipper! Smack!
smack! smack! Three gone before you could wink!' He leaned back
in his chair and laughed his eyes into his head again.
"I was a little startled at the nature of the child's amusement,
but the father's laughter made me think that perhaps he was
joking.
"'My sole duties, then,' I asked, 'are to take charge of a single
child?'
"'No, no, not the sole, not the sole, my dear young lady,' he
cried. 'Your duty would be, as I am sure your good sense would
suggest, to obey any little commands my wife might give, provided
always that they were such commands as a lady might with
propriety obey. You see no difficulty, heh?'
"'I should be happy to make myself useful.'
"'Quite so. In dress now, for example. We are faddy people, you
know--faddy but kind-hearted. If you were asked to wear any dress
which we might give you, you would not object to our little whim.
Heh?'
"'No,' said I, considerably astonished at his words.
"'Or to sit here, or sit there, that would not be offensive to
you?'
"'Oh, no.'
"'Or to cut your hair quite short before you come to us?'
"I could hardly believe my ears. As you may observe, Mr. Holmes,
my hair is somewhat luxuriant, and of a rather peculiar tint of
chestnut. It has been considered artistic. I could not dream of
sacrificing it in this offhand fashion.
"'I am afraid that that is quite impossible,' said I. He had been
watching me eagerly out of his small eyes, and I could see a
shadow pass over his face as I spoke.
"'I am afraid that it is quite essential,' said he. 'It is a
little fancy of my wife's, and ladies' fancies, you know, madam,
ladies' fancies must be consulted. And so you won't cut your
hair?'
"'No, sir, I really could not,' I answered firmly.
"'Ah, very well; then that quite settles the matter. It is a
pity, because in other respects you would really have done very
nicely. In that case, Miss Stoper, I had best inspect a few more
of your young ladies.'
"The manageress had sat all this while busy with her papers
without a word to either of us, but she glanced at me now with so
much annoyance upon her face that I could not help suspecting
that she had lost a handsome commission through my refusal.
"'Do you desire your name to be kept upon the books?' she asked.
"'If you please, Miss Stoper.'
"'Well, really, it seems rather useless, since you refuse the
most excellent offers in this fashion,' said she sharply. 'You
can hardly expect us to exert ourselves to find another such
opening for you. Good-day to you, Miss Hunter.' She struck a gong
upon the table, and I was shown out by the page.
"Well, Mr. Holmes, when I got back to my lodgings and found
little enough in the cupboard, and two or three bills upon the
table, I began to ask myself whether I had not done a very
foolish thing. After all, if these people had strange fads and
expected obedience on the most extraordinary matters, they were
at least ready to pay for their eccentricity. Very few
governesses in England are getting 100 pounds a year. Besides,
what use was my hair to me? Many people are improved by wearing
it short and perhaps I should be among the number. Next day I was
inclined to think that I had made a mistake, and by the day after
I was sure of it. I had almost overcome my pride so far as to go
back to the agency and inquire whether the place was still open
when I received this letter from the gentleman himself. I have it
here and I will read it to you:
"'The Copper Beeches, near Winchester.
"'DEAR MISS HUNTER:--Miss Stoper has very kindly given me your
address, and I write from here to ask you whether you have
reconsidered your decision. My wife is very anxious that you
should come, for she has been much attracted by my description of
you. We are willing to give 30 pounds a quarter, or 120 pounds a
year, so as to recompense you for any little inconvenience which
our fads may cause you. They are not very exacting, after all. My
wife is fond of a particular shade of electric blue and would
like you to wear such a dress indoors in the morning. You need
not, however, go to the expense of purchasing one, as we have one
belonging to my dear daughter Alice (now in Philadelphia), which
would, I should think, fit you very well. Then, as to sitting
here or there, or amusing yourself in any manner indicated, that
need cause you no inconvenience. As regards your hair, it is no
doubt a pity, especially as I could not help remarking its beauty
during our short interview, but I am afraid that I must remain
firm upon this point, and I only hope that the increased salary
may recompense you for the loss. Your duties, as far as the child
is concerned, are very light. Now do try to come, and I shall
meet you with the dog-cart at Winchester. Let me know your train.
Yours faithfully, JEPHRO RUCASTLE.'
"That is the letter which I have just received, Mr. Holmes, and
my mind is made up that I will accept it. I thought, however,
that before taking the final step I should like to submit the
whole matter to your consideration."
"Well, Miss Hunter, if your mind is made up, that settles the
question," said Holmes, smiling.
"But you would not advise me to refuse?"
"I confess that it is not the situation which I should like to
see a sister of mine apply for."
"What is the meaning of it all, Mr. Holmes?"
"Ah, I have no data. I cannot tell. Perhaps you have yourself
formed some opinion?"
"Well, there seems to me to be only one possible solution. Mr.
Rucastle seemed to be a very kind, good-natured man. Is it not
possible that his wife is a lunatic, that he desires to keep the
matter quiet for fear she should be taken to an asylum, and that
he humours her fancies in every way in order to prevent an
outbreak?"
"That is a possible solution--in fact, as matters stand, it is
the most probable one. But in any case it does not seem to be a
nice household for a young lady."
"But the money, Mr. Holmes, the money!"
"Well, yes, of course the pay is good--too good. That is what
makes me uneasy. Why should they give you 120 pounds a year, when
they could have their pick for 40 pounds? There must be some
strong reason behind."
"I thought that if I told you the circumstances you would
understand afterwards if I wanted your help. I should feel so
much stronger if I felt that you were at the back of me."
"Oh, you may carry that feeling away with you. I assure you that
your little problem promises to be the most interesting which has
come my way for some months. There is something distinctly novel
about some of the features. If you should find yourself in doubt
or in danger--"
"Danger! What danger do you foresee?"
Holmes shook his head gravely. "It would cease to be a danger if
we could define it," said he. "But at any time, day or night, a
telegram would bring me down to your help."
"That is enough." She rose briskly from her chair with the
anxiety all swept from her face. "I shall go down to Hampshire
quite easy in my mind now. I shall write to Mr. Rucastle at once,
sacrifice my poor hair to-night, and start for Winchester
to-morrow." With a few grateful words to Holmes she bade us both
good-night and bustled off upon her way.
"At least," said I as we heard her quick, firm steps descending
the stairs, "she seems to be a young lady who is very well able
to take care of herself."
"And she would need to be," said Holmes gravely. "I am much
mistaken if we do not hear from her before many days are past."
It was not very long before my friend's prediction was fulfilled.
A fortnight went by, during which I frequently found my thoughts
turning in her direction and wondering what strange side-alley of
human experience this lonely woman had strayed into. The unusual
salary, the curious conditions, the light duties, all pointed to
something abnormal, though whether a fad or a plot, or whether
the man were a philanthropist or a villain, it was quite beyond
my powers to determine. As to Holmes, I observed that he sat
frequently for half an hour on end, with knitted brows and an
abstracted air, but he swept the matter away with a wave of his
hand when I mentioned it. "Data! data! data!" he cried
impatiently. "I can't make bricks without clay." And yet he would
always wind up by muttering that no sister of his should ever
have accepted such a situation.
The telegram which we eventually received came late one night
just as I was thinking of turning in and Holmes was settling down
to one of those all-night chemical researches which he frequently
indulged in, when I would leave him stooping over a retort and a
test-tube at night and find him in the same position when I came
down to breakfast in the morning. He opened the yellow envelope,
and then, glancing at the message, threw it across to me.
"Just look up the trains in Bradshaw," said he, and turned back
to his chemical studies.
The summons was a brief and urgent one.
"Please be at the Black Swan Hotel at Winchester at midday
to-morrow," it said. "Do come! I am at my wit's end. HUNTER."
"Will you come with me?" asked Holmes, glancing up.
"I should wish to."
"Just look it up, then."
"There is a train at half-past nine," said I, glancing over my
Bradshaw. "It is due at Winchester at 11:30."
"That will do very nicely. Then perhaps I had better postpone my
analysis of the acetones, as we may need to be at our best in the
morning."
By eleven o'clock the next day we were well upon our way to the
old English capital. Holmes had been buried in the morning papers
all the way down, but after we had passed the Hampshire border he
threw them down and began to admire the scenery. It was an ideal
spring day, a light blue sky, flecked with little fleecy white
clouds drifting across from west to east. The sun was shining
very brightly, and yet there was an exhilarating nip in the air,
which set an edge to a man's energy. All over the countryside,
away to the rolling hills around Aldershot, the little red and
grey roofs of the farm-steadings peeped out from amid the light
green of the new foliage.
"Are they not fresh and beautiful?" I cried with all the
enthusiasm of a man fresh from the fogs of Baker Street.
But Holmes shook his head gravely.
"Do you know, Watson," said he, "that it is one of the curses of
a mind with a turn like mine that I must look at everything with
reference to my own special subject. You look at these scattered
houses, and you are impressed by their beauty. I look at them,
and the only thought which comes to me is a feeling of their
isolation and of the impunity with which crime may be committed
there."
"Good heavens!" I cried. "Who would associate crime with these
dear old homesteads?"
"They always fill me with a certain horror. It is my belief,
Watson, founded upon my experience, that the lowest and vilest
alleys in London do not present a more dreadful record of sin
than does the smiling and beautiful countryside."
"You horrify me!"
"But the reason is very obvious. The pressure of public opinion
can do in the town what the law cannot accomplish. There is no
lane so vile that the scream of a tortured child, or the thud of
a drunkard's blow, does not beget sympathy and indignation among
the neighbours, and then the whole machinery of justice is ever
so close that a word of complaint can set it going, and there is
but a step between the crime and the dock. But look at these
lonely houses, each in its own fields, filled for the most part
with poor ignorant folk who know little of the law. Think of the
deeds of hellish cruelty, the hidden wickedness which may go on,
year in, year out, in such places, and none the wiser. Had this
lady who appeals to us for help gone to live in Winchester, I
should never have had a fear for her. It is the five miles of
country which makes the danger. Still, it is clear that she is
not personally threatened."
"No. If she can come to Winchester to meet us she can get away."
"Quite so. She has her freedom."
"What CAN be the matter, then? Can you suggest no explanation?"
"I have devised seven separate explanations, each of which would
cover the facts as far as we know them. But which of these is
correct can only be determined by the fresh information which we
shall no doubt find waiting for us. Well, there is the tower of
the cathedral, and we shall soon learn all that Miss Hunter has
to tell."
The Black Swan is an inn of repute in the High Street, at no
distance from the station, and there we found the young lady
waiting for us. She had engaged a sitting-room, and our lunch
awaited us upon the table.
"I am so delighted that you have come," she said earnestly. "It
is so very kind of you both; but indeed I do not know what I
should do. Your advice will be altogether invaluable to me."
"Pray tell us what has happened to you."
"I will do so, and I must be quick, for I have promised Mr.
Rucastle to be back before three. I got his leave to come into
town this morning, though he little knew for what purpose."
"Let us have everything in its due order." Holmes thrust his long
thin legs out towards the fire and composed himself to listen.
"In the first place, I may say that I have met, on the whole,
with no actual ill-treatment from Mr. and Mrs. Rucastle. It is
only fair to them to say that. But I cannot understand them, and
I am not easy in my mind about them."
"What can you not understand?"
"Their reasons for their conduct. But you shall have it all just
as it occurred. When I came down, Mr. Rucastle met me here and
drove me in his dog-cart to the Copper Beeches. It is, as he
said, beautifully situated, but it is not beautiful in itself,
for it is a large square block of a house, whitewashed, but all
stained and streaked with damp and bad weather. There are grounds
round it, woods on three sides, and on the fourth a field which
slopes down to the Southampton highroad, which curves past about
a hundred yards from the front door. This ground in front belongs
to the house, but the woods all round are part of Lord
Southerton's preserves. A clump of copper beeches immediately in
front of the hall door has given its name to the place.
"I was driven over by my employer, who was as amiable as ever,
and was introduced by him that evening to his wife and the child.
There was no truth, Mr. Holmes, in the conjecture which seemed to
us to be probable in your rooms at Baker Street. Mrs. Rucastle is
not mad. I found her to be a silent, pale-faced woman, much
younger than her husband, not more than thirty, I should think,
while he can hardly be less than forty-five. From their
conversation I have gathered that they have been married about
seven years, that he was a widower, and that his only child by
the first wife was the daughter who has gone to Philadelphia. Mr.
Rucastle told me in private that the reason why she had left them
was that she had an unreasoning aversion to her stepmother. As
the daughter could not have been less than twenty, I can quite
imagine that her position must have been uncomfortable with her
father's young wife.
"Mrs. Rucastle seemed to me to be colourless in mind as well as
in feature. She impressed me neither favourably nor the reverse.
She was a nonentity. It was easy to see that she was passionately
devoted both to her husband and to her little son. Her light grey
eyes wandered continually from one to the other, noting every
little want and forestalling it if possible. He was kind to her
also in his bluff, boisterous fashion, and on the whole they
seemed to be a happy couple. And yet she had some secret sorrow,
this woman. She would often be lost in deep thought, with the
saddest look upon her face. More than once I have surprised her
in tears. I have thought sometimes that it was the disposition of
her child which weighed upon her mind, for I have never met so
utterly spoiled and so ill-natured a little creature. He is small
for his age, with a head which is quite disproportionately large.
His whole life appears to be spent in an alternation between
savage fits of passion and gloomy intervals of sulking. Giving
pain to any creature weaker than himself seems to be his one idea
of amusement, and he shows quite remarkable talent in planning
the capture of mice, little birds, and insects. But I would
rather not talk about the creature, Mr. Holmes, and, indeed, he
has little to do with my story."
"I am glad of all details," remarked my friend, "whether they
seem to you to be relevant or not."
"I shall try not to miss anything of importance. The one
unpleasant thing about the house, which struck me at once, was
the appearance and conduct of the servants. There are only two, a
man and his wife. Toller, for that is his name, is a rough,
uncouth man, with grizzled hair and whiskers, and a perpetual
smell of drink. Twice since I have been with them he has been
quite drunk, and yet Mr. Rucastle seemed to take no notice of it.
His wife is a very tall and strong woman with a sour face, as
silent as Mrs. Rucastle and much less amiable. They are a most
unpleasant couple, but fortunately I spend most of my time in the
nursery and my own room, which are next to each other in one
corner of the building.
"For two days after my arrival at the Copper Beeches my life was
very quiet; on the third, Mrs. Rucastle came down just after
breakfast and whispered something to her husband.
"'Oh, yes,' said he, turning to me, 'we are very much obliged to
you, Miss Hunter, for falling in with our whims so far as to cut
your hair. I assure you that it has not detracted in the tiniest
iota from your appearance. We shall now see how the electric-blue
dress will become you. You will find it laid out upon the bed in
your room, and if you would be so good as to put it on we should
both be extremely obliged.'
"The dress which I found waiting for me was of a peculiar shade
of blue. It was of excellent material, a sort of beige, but it
bore unmistakable signs of having been worn before. It could not
have been a better fit if I had been measured for it. Both Mr.
and Mrs. Rucastle expressed a delight at the look of it, which
seemed quite exaggerated in its vehemence. They were waiting for
me in the drawing-room, which is a very large room, stretching
along the entire front of the house, with three long windows
reaching down to the floor. A chair had been placed close to the
central window, with its back turned towards it. In this I was
asked to sit, and then Mr. Rucastle, walking up and down on the
other side of the room, began to tell me a series of the funniest
stories that I have ever listened to. You cannot imagine how
comical he was, and I laughed until I was quite weary. Mrs.
Rucastle, however, who has evidently no sense of humour, never so
much as smiled, but sat with her hands in her lap, and a sad,
anxious look upon her face. After an hour or so, Mr. Rucastle
suddenly remarked that it was time to commence the duties of the
day, and that I might change my dress and go to little Edward in
the nursery.
"Two days later this same performance was gone through under
exactly similar circumstances. Again I changed my dress, again I
sat in the window, and again I laughed very heartily at the funny
stories of which my employer had an immense répertoire, and which
he told inimitably. Then he handed me a yellow-backed novel, and
moving my chair a little sideways, that my own shadow might not
fall upon the page, he begged me to read aloud to him. I read for
about ten minutes, beginning in the heart of a chapter, and then
suddenly, in the middle of a sentence, he ordered me to cease and
to change my dress.
"You can easily imagine, Mr. Holmes, how curious I became as to
what the meaning of this extraordinary performance could possibly
be. They were always very careful, I observed, to turn my face
away from the window, so that I became consumed with the desire
to see what was going on behind my back. At first it seemed to be
impossible, but I soon devised a means. My hand-mirror had been
broken, so a happy thought seized me, and I concealed a piece of
the glass in my handkerchief. On the next occasion, in the midst
of my laughter, I put my handkerchief up to my eyes, and was able
with a little management to see all that there was behind me. I
confess that I was disappointed. There was nothing. At least that
was my first impression. At the second glance, however, I
perceived that there was a man standing in the Southampton Road,
a small bearded man in a grey suit, who seemed to be looking in
my direction. The road is an important highway, and there are
usually people there. This man, however, was leaning against the
railings which bordered our field and was looking earnestly up. I
lowered my handkerchief and glanced at Mrs. Rucastle to find her
eyes fixed upon me with a most searching gaze. She said nothing,
but I am convinced that she had divined that I had a mirror in my
hand and had seen what was behind me. She rose at once.
"'Jephro,' said she, 'there is an impertinent fellow upon the
road there who stares up at Miss Hunter.'
"'No friend of yours, Miss Hunter?' he asked.
"'No, I know no one in these parts.'
"'Dear me! How very impertinent! Kindly turn round and motion to
him to go away.'
"'Surely it would be better to take no notice.'
"'No, no, we should have him loitering here always. Kindly turn
round and wave him away like that.'
"I did as I was told, and at the same instant Mrs. Rucastle drew
down the blind. That was a week ago, and from that time I have
not sat again in the window, nor have I worn the blue dress, nor
seen the man in the road."
"Pray continue," said Holmes. "Your narrative promises to be a
most interesting one."
"You will find it rather disconnected, I fear, and there may
prove to be little relation between the different incidents of
which I speak. On the very first day that I was at the Copper
Beeches, Mr. Rucastle took me to a small outhouse which stands
near the kitchen door. As we approached it I heard the sharp
rattling of a chain, and the sound as of a large animal moving
about.
"'Look in here!' said Mr. Rucastle, showing me a slit between two
planks. 'Is he not a beauty?'
"I looked through and was conscious of two glowing eyes, and of a
vague figure huddled up in the darkness.
"'Don't be frightened,' said my employer, laughing at the start
which I had given. 'It's only Carlo, my mastiff. I call him mine,
but really old Toller, my groom, is the only man who can do
anything with him. We feed him once a day, and not too much then,
so that he is always as keen as mustard. Toller lets him loose
every night, and God help the trespasser whom he lays his fangs
upon. For goodness' sake don't you ever on any pretext set your
foot over the threshold at night, for it's as much as your life
is worth.'
"The warning was no idle one, for two nights later I happened to
look out of my bedroom window about two o'clock in the morning.
It was a beautiful moonlight night, and the lawn in front of the
house was silvered over and almost as bright as day. I was
standing, rapt in the peaceful beauty of the scene, when I was
aware that something was moving under the shadow of the copper
beeches. As it emerged into the moonshine I saw what it was. It
was a giant dog, as large as a calf, tawny tinted, with hanging
jowl, black muzzle, and huge projecting bones. It walked slowly
across the lawn and vanished into the shadow upon the other side.
That dreadful sentinel sent a chill to my heart which I do not
think that any burglar could have done.
"And now I have a very strange experience to tell you. I had, as
you know, cut off my hair in London, and I had placed it in a
great coil at the bottom of my trunk. One evening, after the
child was in bed, I began to amuse myself by examining the
furniture of my room and by rearranging my own little things.
There was an old chest of drawers in the room, the two upper ones
empty and open, the lower one locked. I had filled the first two
with my linen, and as I had still much to pack away I was
naturally annoyed at not having the use of the third drawer. It
struck me that it might have been fastened by a mere oversight,
so I took out my bunch of keys and tried to open it. The very
first key fitted to perfection, and I drew the drawer open. There
was only one thing in it, but I am sure that you would never
guess what it was. It was my coil of hair.
"I took it up and examined it. It was of the same peculiar tint,
and the same thickness. But then the impossibility of the thing
obtruded itself upon me. How could my hair have been locked in
the drawer? With trembling hands I undid my trunk, turned out the
contents, and drew from the bottom my own hair. I laid the two
tresses together, and I assure you that they were identical. Was
it not extraordinary? Puzzle as I would, I could make nothing at
all of what it meant. I returned the strange hair to the drawer,
and I said nothing of the matter to the Rucastles as I felt that
I had put myself in the wrong by opening a drawer which they had
locked.
"I am naturally observant, as you may have remarked, Mr. Holmes,
and I soon had a pretty good plan of the whole house in my head.
There was one wing, however, which appeared not to be inhabited
at all. A door which faced that which led into the quarters of
the Tollers opened into this suite, but it was invariably locked.
One day, however, as I ascended the stair, I met Mr. Rucastle
coming out through this door, his keys in his hand, and a look on
his face which made him a very different person to the round,
jovial man to whom I was accustomed. His cheeks were red, his
brow was all crinkled with anger, and the veins stood out at his
temples with passion. He locked the door and hurried past me
without a word or a look.
"This aroused my curiosity, so when I went out for a walk in the
grounds with my charge, I strolled round to the side from which I
could see the windows of this part of the house. There were four
of them in a row, three of which were simply dirty, while the
fourth was shuttered up. They were evidently all deserted. As I
strolled up and down, glancing at them occasionally, Mr. Rucastle
came out to me, looking as merry and jovial as ever.
"'Ah!' said he, 'you must not think me rude if I passed you
without a word, my dear young lady. I was preoccupied with
business matters.'
"I assured him that I was not offended. 'By the way,' said I,
'you seem to have quite a suite of spare rooms up there, and one
of them has the shutters up.'
"He looked surprised and, as it seemed to me, a little startled
at my remark.
"'Photography is one of my hobbies,' said he. 'I have made my
dark room up there. But, dear me! what an observant young lady we
have come upon. Who would have believed it? Who would have ever
believed it?' He spoke in a jesting tone, but there was no jest
in his eyes as he looked at me. I read suspicion there and
annoyance, but no jest.
"Well, Mr. Holmes, from the moment that I understood that there
was something about that suite of rooms which I was not to know,
I was all on fire to go over them. It was not mere curiosity,
though I have my share of that. It was more a feeling of duty--a
feeling that some good might come from my penetrating to this
place. They talk of woman's instinct; perhaps it was woman's
instinct which gave me that feeling. At any rate, it was there,
and I was keenly on the lookout for any chance to pass the
forbidden door.
"It was only yesterday that the chance came. I may tell you that,
besides Mr. Rucastle, both Toller and his wife find something to
do in these deserted rooms, and I once saw him carrying a large
black linen bag with him through the door. Recently he has been
drinking hard, and yesterday evening he was very drunk; and when
I came upstairs there was the key in the door. I have no doubt at
all that he had left it there. Mr. and Mrs. Rucastle were both
downstairs, and the child was with them, so that I had an
admirable opportunity. I turned the key gently in the lock,
opened the door, and slipped through.
"There was a little passage in front of me, unpapered and
uncarpeted, which turned at a right angle at the farther end.
Round this corner were three doors in a line, the first and third
of which were open. They each led into an empty room, dusty and
cheerless, with two windows in the one and one in the other, so
thick with dirt that the evening light glimmered dimly through
them. The centre door was closed, and across the outside of it
had been fastened one of the broad bars of an iron bed, padlocked
at one end to a ring in the wall, and fastened at the other with
stout cord. The door itself was locked as well, and the key was
not there. This barricaded door corresponded clearly with the
shuttered window outside, and yet I could see by the glimmer from
beneath it that the room was not in darkness. Evidently there was
a skylight which let in light from above. As I stood in the
passage gazing at the sinister door and wondering what secret it
might veil, I suddenly heard the sound of steps within the room
and saw a shadow pass backward and forward against the little
slit of dim light which shone out from under the door. A mad,
unreasoning terror rose up in me at the sight, Mr. Holmes. My
overstrung nerves failed me suddenly, and I turned and ran--ran
as though some dreadful hand were behind me clutching at the
skirt of my dress. I rushed down the passage, through the door,
and straight into the arms of Mr. Rucastle, who was waiting
outside.
"'So,' said he, smiling, 'it was you, then. I thought that it
must be when I saw the door open.'
"'Oh, I am so frightened!' I panted.
"'My dear young lady! my dear young lady!'--you cannot think how
caressing and soothing his manner was--'and what has frightened
you, my dear young lady?'
"But his voice was just a little too coaxing. He overdid it. I
was keenly on my guard against him.
"'I was foolish enough to go into the empty wing,' I answered.
'But it is so lonely and eerie in this dim light that I was
frightened and ran out again. Oh, it is so dreadfully still in
there!'
"'Only that?' said he, looking at me keenly.
"'Why, what did you think?' I asked.
"'Why do you think that I lock this door?'
"'I am sure that I do not know.'
"'It is to keep people out who have no business there. Do you
see?' He was still smiling in the most amiable manner.
"'I am sure if I had known--'
"'Well, then, you know now. And if you ever put your foot over
that threshold again'--here in an instant the smile hardened into
a grin of rage, and he glared down at me with the face of a
demon--'I'll throw you to the mastiff.'
"I was so terrified that I do not know what I did. I suppose that
I must have rushed past him into my room. I remember nothing
until I found myself lying on my bed trembling all over. Then I
thought of you, Mr. Holmes. I could not live there longer without
some advice. I was frightened of the house, of the man, of the
woman, of the servants, even of the child. They were all horrible
to me. If I could only bring you down all would be well. Of
course I might have fled from the house, but my curiosity was
almost as strong as my fears. My mind was soon made up. I would
send you a wire. I put on my hat and cloak, went down to the
office, which is about half a mile from the house, and then
returned, feeling very much easier. A horrible doubt came into my
mind as I approached the door lest the dog might be loose, but I
remembered that Toller had drunk himself into a state of
insensibility that evening, and I knew that he was the only one
in the household who had any influence with the savage creature,
or who would venture to set him free. I slipped in in safety and
lay awake half the night in my joy at the thought of seeing you.
I had no difficulty in getting leave to come into Winchester this
morning, but I must be back before three o'clock, for Mr. and
Mrs. Rucastle are going on a visit, and will be away all the
evening, so that I must look after the child. Now I have told you
all my adventures, Mr. Holmes, and I should be very glad if you
could tell me what it all means, and, above all, what I should
do."
Holmes and I had listened spellbound to this extraordinary story.
My friend rose now and paced up and down the room, his hands in
his pockets, and an expression of the most profound gravity upon
his face.
"Is Toller still drunk?" he asked.
"Yes. I heard his wife tell Mrs. Rucastle that she could do
nothing with him."
"That is well. And the Rucastles go out to-night?"
"Yes."
"Is there a cellar with a good strong lock?"
"Yes, the wine-cellar."
"You seem to me to have acted all through this matter like a very
brave and sensible girl, Miss Hunter. Do you think that you could
perform one more feat? I should not ask it of you if I did not
think you a quite exceptional woman."
"I will try. What is it?"
"We shall be at the Copper Beeches by seven o'clock, my friend
and I. The Rucastles will be gone by that time, and Toller will,
we hope, be incapable. There only remains Mrs. Toller, who might
give the alarm. If you could send her into the cellar on some
errand, and then turn the key upon her, you would facilitate
matters immensely."
"I will do it."
"Excellent! We shall then look thoroughly into the affair. Of
course there is only one feasible explanation. You have been
brought there to personate someone, and the real person is
imprisoned in this chamber. That is obvious. As to who this
prisoner is, I have no doubt that it is the daughter, Miss Alice
Rucastle, if I remember right, who was said to have gone to
America. You were chosen, doubtless, as resembling her in height,
figure, and the colour of your hair. Hers had been cut off, very
possibly in some illness through which she has passed, and so, of
course, yours had to be sacrificed also. By a curious chance you
came upon her tresses. The man in the road was undoubtedly some
friend of hers--possibly her fiancé--and no doubt, as you wore
the girl's dress and were so like her, he was convinced from your
laughter, whenever he saw you, and afterwards from your gesture,
that Miss Rucastle was perfectly happy, and that she no longer
desired his attentions. The dog is let loose at night to prevent
him from endeavouring to communicate with her. So much is fairly
clear. The most serious point in the case is the disposition of
the child."
"What on earth has that to do with it?" I ejaculated.
"My dear Watson, you as a medical man are continually gaining
light as to the tendencies of a child by the study of the
parents. Don't you see that the converse is equally valid. I have
frequently gained my first real insight into the character of
parents by studying their children. This child's disposition is
abnormally cruel, merely for cruelty's sake, and whether he
derives this from his smiling father, as I should suspect, or
from his mother, it bodes evil for the poor girl who is in their
power."
"I am sure that you are right, Mr. Holmes," cried our client. "A
thousand things come back to me which make me certain that you
have hit it. Oh, let us lose not an instant in bringing help to
this poor creature."
"We must be circumspect, for we are dealing with a very cunning
man. We can do nothing until seven o'clock. At that hour we shall
be with you, and it will not be long before we solve the
mystery."
We were as good as our word, for it was just seven when we
reached the Copper Beeches, having put up our trap at a wayside
public-house. The group of trees, with their dark leaves shining
like burnished metal in the light of the setting sun, were
sufficient to mark the house even had Miss Hunter not been
standing smiling on the door-step.
"Have you managed it?" asked Holmes.
A loud thudding noise came from somewhere downstairs. "That is
Mrs. Toller in the cellar," said she. "Her husband lies snoring
on the kitchen rug. Here are his keys, which are the duplicates
of Mr. Rucastle's."
"You have done well indeed!" cried Holmes with enthusiasm. "Now
lead the way, and we shall soon see the end of this black
business."
We passed up the stair, unlocked the door, followed on down a
passage, and found ourselves in front of the barricade which Miss
Hunter had described. Holmes cut the cord and removed the
transverse bar. Then he tried the various keys in the lock, but
without success. No sound came from within, and at the silence
Holmes' face clouded over.
"I trust that we are not too late," said he. "I think, Miss
Hunter, that we had better go in without you. Now, Watson, put
your shoulder to it, and we shall see whether we cannot make our
way in."
It was an old rickety door and gave at once before our united
strength. Together we rushed into the room. It was empty. There
was no furniture save a little pallet bed, a small table, and a
basketful of linen. The skylight above was open, and the prisoner
gone.
"There has been some villainy here," said Holmes; "this beauty
has guessed Miss Hunter's intentions and has carried his victim
off."
"But how?"
"Through the skylight. We shall soon see how he managed it." He
swung himself up onto the roof. "Ah, yes," he cried, "here's the
end of a long light ladder against the eaves. That is how he did
it."
"But it is impossible," said Miss Hunter; "the ladder was not
there when the Rucastles went away."
"He has come back and done it. I tell you that he is a clever and
dangerous man. I should not be very much surprised if this were
he whose step I hear now upon the stair. I think, Watson, that it
would be as well for you to have your pistol ready."
The words were hardly out of his mouth before a man appeared at
the door of the room, a very fat and burly man, with a heavy
stick in his hand. Miss Hunter screamed and shrunk against the
wall at the sight of him, but Sherlock Holmes sprang forward and
confronted him.
"You villain!" said he, "where's your daughter?"
The fat man cast his eyes round, and then up at the open
skylight.
"It is for me to ask you that," he shrieked, "you thieves! Spies
and thieves! I have caught you, have I? You are in my power. I'll
serve you!" He turned and clattered down the stairs as hard as he
could go.
"He's gone for the dog!" cried Miss Hunter.
"I have my revolver," said I.
"Better close the front door," cried Holmes, and we all rushed
down the stairs together. We had hardly reached the hall when we
heard the baying of a hound, and then a scream of agony, with a
horrible worrying sound which it was dreadful to listen to. An
elderly man with a red face and shaking limbs came staggering out
at a side door.
"My God!" he cried. "Someone has loosed the dog. It's not been
fed for two days. Quick, quick, or it'll be too late!"
Holmes and I rushed out and round the angle of the house, with
Toller hurrying behind us. There was the huge famished brute, its
black muzzle buried in Rucastle's throat, while he writhed and
screamed upon the ground. Running up, I blew its brains out, and
it fell over with its keen white teeth still meeting in the great
creases of his neck. With much labour we separated them and
carried him, living but horribly mangled, into the house. We laid
him upon the drawing-room sofa, and having dispatched the sobered
Toller to bear the news to his wife, I did what I could to
relieve his pain. We were all assembled round him when the door
opened, and a tall, gaunt woman entered the room.
"Mrs. Toller!" cried Miss Hunter.
"Yes, miss. Mr. Rucastle let me out when he came back before he
went up to you. Ah, miss, it is a pity you didn't let me know
what you were planning, for I would have told you that your pains
were wasted."
"Ha!" said Holmes, looking keenly at her. "It is clear that Mrs.
Toller knows more about this matter than anyone else."
"Yes, sir, I do, and I am ready enough to tell what I know."
"Then, pray, sit down, and let us hear it for there are several
points on which I must confess that I am still in the dark."
"I will soon make it clear to you," said she; "and I'd have done
so before now if I could ha' got out from the cellar. If there's
police-court business over this, you'll remember that I was the
one that stood your friend, and that I was Miss Alice's friend
too.
"She was never happy at home, Miss Alice wasn't, from the time
that her father married again. She was slighted like and had no
say in anything, but it never really became bad for her until
after she met Mr. Fowler at a friend's house. As well as I could
learn, Miss Alice had rights of her own by will, but she was so
quiet and patient, she was, that she never said a word about them
but just left everything in Mr. Rucastle's hands. He knew he was
safe with her; but when there was a chance of a husband coming
forward, who would ask for all that the law would give him, then
her father thought it time to put a stop on it. He wanted her to
sign a paper, so that whether she married or not, he could use
her money. When she wouldn't do it, he kept on worrying her until
she got brain-fever, and for six weeks was at death's door. Then
she got better at last, all worn to a shadow, and with her
beautiful hair cut off; but that didn't make no change in her
young man, and he stuck to her as true as man could be."
"Ah," said Holmes, "I think that what you have been good enough
to tell us makes the matter fairly clear, and that I can deduce
all that remains. Mr. Rucastle then, I presume, took to this
system of imprisonment?"
"Yes, sir."
"And brought Miss Hunter down from London in order to get rid of
the disagreeable persistence of Mr. Fowler."
"That was it, sir."
"But Mr. Fowler being a persevering man, as a good seaman should
be, blockaded the house, and having met you succeeded by certain
arguments, metallic or otherwise, in convincing you that your
interests were the same as his."
"Mr. Fowler was a very kind-spoken, free-handed gentleman," said
Mrs. Toller serenely.
"And in this way he managed that your good man should have no
want of drink, and that a ladder should be ready at the moment
when your master had gone out."
"You have it, sir, just as it happened."
"I am sure we owe you an apology, Mrs. Toller," said Holmes, "for
you have certainly cleared up everything which puzzled us. And
here comes the country surgeon and Mrs. Rucastle, so I think,
Watson, that we had best escort Miss Hunter back to Winchester,
as it seems to me that our locus standi now is rather a
questionable one."
And thus was solved the mystery of the sinister house with the
copper beeches in front of the door. Mr. Rucastle survived, but
was always a broken man, kept alive solely through the care of
his devoted wife. They still live with their old servants, who
probably know so much of Rucastle's past life that he finds it
difficult to part from them. Mr. Fowler and Miss Rucastle were
married, by special license, in Southampton the day after their
flight, and he is now the holder of a government appointment in
the island of Mauritius. As to Miss Violet Hunter, my friend
Holmes, rather to my disappointment, manifested no further
interest in her when once she had ceased to be the centre of one
of his problems, and she is now the head of a private school at
Walsall, where I believe that she has met with considerable success.
End of the Project Gutenberg EBook of The Adventures of Sherlock Holmes, by
Arthur Conan Doyle
*** END OF THIS PROJECT GUTENBERG EBOOK THE ADVENTURES OF SHERLOCK HOLMES ***
***** This file should be named 1661-8.txt or 1661-8.zip *****
This and all associated files of various formats will be found in:
http://www.gutenberg.org/1/6/6/1661/
Produced by an anonymous Project Gutenberg volunteer and Jose Menendez
Updated editions will replace the previous one--the old editions
will be renamed.
Creating the works from public domain print editions means that no
one owns a United States copyright in these works, so the Foundation
(and you!) can copy and distribute it in the United States without
permission and without paying copyright royalties. Special rules,
set forth in the General Terms of Use part of this license, apply to
copying and distributing Project Gutenberg-tm electronic works to
protect the PROJECT GUTENBERG-tm concept and trademark. Project
Gutenberg is a registered trademark, and may not be used if you
charge for the eBooks, unless you receive specific permission. If you
do not charge anything for copies of this eBook, complying with the
rules is very easy. You may use this eBook for nearly any purpose
such as creation of derivative works, reports, performances and
research. They may be modified and printed and given away--you may do
practically ANYTHING with public domain eBooks. Redistribution is
subject to the trademark license, especially commercial
redistribution.
*** START: FULL LICENSE ***
THE FULL PROJECT GUTENBERG LICENSE
PLEASE READ THIS BEFORE YOU DISTRIBUTE OR USE THIS WORK
To protect the Project Gutenberg-tm mission of promoting the free
distribution of electronic works, by using or distributing this work
(or any other work associated in any way with the phrase "Project
Gutenberg"), you agree to comply with all the terms of the Full Project
Gutenberg-tm License (available with this file or online at
http://gutenberg.net/license).
Section 1. General Terms of Use and Redistributing Project Gutenberg-tm
electronic works
1.A. By reading or using any part of this Project Gutenberg-tm
electronic work, you indicate that you have read, understand, agree to
and accept all the terms of this license and intellectual property
(trademark/copyright) agreement. If you do not agree to abide by all
the terms of this agreement, you must cease using and return or destroy
all copies of Project Gutenberg-tm electronic works in your possession.
If you paid a fee for obtaining a copy of or access to a Project
Gutenberg-tm electronic work and you do not agree to be bound by the
terms of this agreement, you may obtain a refund from the person or
entity to whom you paid the fee as set forth in paragraph 1.E.8.
1.B. "Project Gutenberg" is a registered trademark. It may only be
used on or associated in any way with an electronic work by people who
agree to be bound by the terms of this agreement. There are a few
things that you can do with most Project Gutenberg-tm electronic works
even without complying with the full terms of this agreement. See
paragraph 1.C below. There are a lot of things you can do with Project
Gutenberg-tm electronic works if you follow the terms of this agreement
and help preserve free future access to Project Gutenberg-tm electronic
works. See paragraph 1.E below.
1.C. The Project Gutenberg Literary Archive Foundation ("the Foundation"
or PGLAF), owns a compilation copyright in the collection of Project
Gutenberg-tm electronic works. Nearly all the individual works in the
collection are in the public domain in the United States. If an
individual work is in the public domain in the United States and you are
located in the United States, we do not claim a right to prevent you from
copying, distributing, performing, displaying or creating derivative
works based on the work as long as all references to Project Gutenberg
are removed. Of course, we hope that you will support the Project
Gutenberg-tm mission of promoting free access to electronic works by
freely sharing Project Gutenberg-tm works in compliance with the terms of
this agreement for keeping the Project Gutenberg-tm name associated with
the work. You can easily comply with the terms of this agreement by
keeping this work in the same format with its attached full Project
Gutenberg-tm License when you share it without charge with others.
1.D. The copyright laws of the place where you are located also govern
what you can do with this work. Copyright laws in most countries are in
a constant state of change. If you are outside the United States, check
the laws of your country in addition to the terms of this agreement
before downloading, copying, displaying, performing, distributing or
creating derivative works based on this work or any other Project
Gutenberg-tm work. The Foundation makes no representations concerning
the copyright status of any work in any country outside the United
States.
1.E. Unless you have removed all references to Project Gutenberg:
1.E.1. The following sentence, with active links to, or other immediate
access to, the full Project Gutenberg-tm License must appear prominently
whenever any copy of a Project Gutenberg-tm work (any work on which the
phrase "Project Gutenberg" appears, or with which the phrase "Project
Gutenberg" is associated) is accessed, displayed, performed, viewed,
copied or distributed:
This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever. You may copy it, give it away or
re-use it under the terms of the Project Gutenberg License included
with this eBook or online at www.gutenberg.net
1.E.2. If an individual Project Gutenberg-tm electronic work is derived
from the public domain (does not contain a notice indicating that it is
posted with permission of the copyright holder), the work can be copied
and distributed to anyone in the United States without paying any fees
or charges. If you are redistributing or providing access to a work
with the phrase "Project Gutenberg" associated with or appearing on the
work, you must comply either with the requirements of paragraphs 1.E.1
through 1.E.7 or obtain permission for the use of the work and the
Project Gutenberg-tm trademark as set forth in paragraphs 1.E.8 or
1.E.9.
1.E.3. If an individual Project Gutenberg-tm electronic work is posted
with the permission of the copyright holder, your use and distribution
must comply with both paragraphs 1.E.1 through 1.E.7 and any additional
terms imposed by the copyright holder. Additional terms will be linked
to the Project Gutenberg-tm License for all works posted with the
permission of the copyright holder found at the beginning of this work.
1.E.4. Do not unlink or detach or remove the full Project Gutenberg-tm
License terms from this work, or any files containing a part of this
work or any other work associated with Project Gutenberg-tm.
1.E.5. Do not copy, display, perform, distribute or redistribute this
electronic work, or any part of this electronic work, without
prominently displaying the sentence set forth in paragraph 1.E.1 with
active links or immediate access to the full terms of the Project
Gutenberg-tm License.
1.E.6. You may convert to and distribute this work in any binary,
compressed, marked up, nonproprietary or proprietary form, including any
word processing or hypertext form. However, if you provide access to or
distribute copies of a Project Gutenberg-tm work in a format other than
"Plain Vanilla ASCII" or other format used in the official version
posted on the official Project Gutenberg-tm web site (www.gutenberg.net),
you must, at no additional cost, fee or expense to the user, provide a
copy, a means of exporting a copy, or a means of obtaining a copy upon
request, of the work in its original "Plain Vanilla ASCII" or other
form. Any alternate format must include the full Project Gutenberg-tm
License as specified in paragraph 1.E.1.
1.E.7. Do not charge a fee for access to, viewing, displaying,
performing, copying or distributing any Project Gutenberg-tm works
unless you comply with paragraph 1.E.8 or 1.E.9.
1.E.8. You may charge a reasonable fee for copies of or providing
access to or distributing Project Gutenberg-tm electronic works provided
that
- You pay a royalty fee of 20% of the gross profits you derive from
the use of Project Gutenberg-tm works calculated using the method
you already use to calculate your applicable taxes. The fee is
owed to the owner of the Project Gutenberg-tm trademark, but he
has agreed to donate royalties under this paragraph to the
Project Gutenberg Literary Archive Foundation. Royalty payments
must be paid within 60 days following each date on which you
prepare (or are legally required to prepare) your periodic tax
returns. Royalty payments should be clearly marked as such and
sent to the Project Gutenberg Literary Archive Foundation at the
address specified in Section 4, "Information about donations to
the Project Gutenberg Literary Archive Foundation."
- You provide a full refund of any money paid by a user who notifies
you in writing (or by e-mail) within 30 days of receipt that s/he
does not agree to the terms of the full Project Gutenberg-tm
License. You must require such a user to return or
destroy all copies of the works possessed in a physical medium
and discontinue all use of and all access to other copies of
Project Gutenberg-tm works.
- You provide, in accordance with paragraph 1.F.3, a full refund of any
money paid for a work or a replacement copy, if a defect in the
electronic work is discovered and reported to you within 90 days
of receipt of the work.
- You comply with all other terms of this agreement for free
distribution of Project Gutenberg-tm works.
1.E.9. If you wish to charge a fee or distribute a Project Gutenberg-tm
electronic work or group of works on different terms than are set
forth in this agreement, you must obtain permission in writing from
both the Project Gutenberg Literary Archive Foundation and Michael
Hart, the owner of the Project Gutenberg-tm trademark. Contact the
Foundation as set forth in Section 3 below.
1.F.
1.F.1. Project Gutenberg volunteers and employees expend considerable
effort to identify, do copyright research on, transcribe and proofread
public domain works in creating the Project Gutenberg-tm
collection. Despite these efforts, Project Gutenberg-tm electronic
works, and the medium on which they may be stored, may contain
"Defects," such as, but not limited to, incomplete, inaccurate or
corrupt data, transcription errors, a copyright or other intellectual
property infringement, a defective or damaged disk or other medium, a
computer virus, or computer codes that damage or cannot be read by
your equipment.
1.F.2. LIMITED WARRANTY, DISCLAIMER OF DAMAGES - Except for the "Right
of Replacement or Refund" described in paragraph 1.F.3, the Project
Gutenberg Literary Archive Foundation, the owner of the Project
Gutenberg-tm trademark, and any other party distributing a Project
Gutenberg-tm electronic work under this agreement, disclaim all
liability to you for damages, costs and expenses, including legal
fees. YOU AGREE THAT YOU HAVE NO REMEDIES FOR NEGLIGENCE, STRICT
LIABILITY, BREACH OF WARRANTY OR BREACH OF CONTRACT EXCEPT THOSE
PROVIDED IN PARAGRAPH 1.F.3. YOU AGREE THAT THE FOUNDATION, THE
TRADEMARK OWNER, AND ANY DISTRIBUTOR UNDER THIS AGREEMENT WILL NOT BE
LIABLE TO YOU FOR ACTUAL, DIRECT, INDIRECT, CONSEQUENTIAL, PUNITIVE OR
INCIDENTAL DAMAGES EVEN IF YOU GIVE NOTICE OF THE POSSIBILITY OF SUCH
DAMAGE.
1.F.3. LIMITED RIGHT OF REPLACEMENT OR REFUND - If you discover a
defect in this electronic work within 90 days of receiving it, you can
receive a refund of the money (if any) you paid for it by sending a
written explanation to the person you received the work from. If you
received the work on a physical medium, you must return the medium with
your written explanation. The person or entity that provided you with
the defective work may elect to provide a replacement copy in lieu of a
refund. If you received the work electronically, the person or entity
providing it to you may choose to give you a second opportunity to
receive the work electronically in lieu of a refund. If the second copy
is also defective, you may demand a refund in writing without further
opportunities to fix the problem.
1.F.4. Except for the limited right of replacement or refund set forth
in paragraph 1.F.3, this work is provided to you 'AS-IS' WITH NO OTHER
WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
WARRANTIES OF MERCHANTIBILITY OR FITNESS FOR ANY PURPOSE.
1.F.5. Some states do not allow disclaimers of certain implied
warranties or the exclusion or limitation of certain types of damages.
If any disclaimer or limitation set forth in this agreement violates the
law of the state applicable to this agreement, the agreement shall be
interpreted to make the maximum disclaimer or limitation permitted by
the applicable state law. The invalidity or unenforceability of any
provision of this agreement shall not void the remaining provisions.
1.F.6. INDEMNITY - You agree to indemnify and hold the Foundation, the
trademark owner, any agent or employee of the Foundation, anyone
providing copies of Project Gutenberg-tm electronic works in accordance
with this agreement, and any volunteers associated with the production,
promotion and distribution of Project Gutenberg-tm electronic works,
harmless from all liability, costs and expenses, including legal fees,
that arise directly or indirectly from any of the following which you do
or cause to occur: (a) distribution of this or any Project Gutenberg-tm
work, (b) alteration, modification, or additions or deletions to any
Project Gutenberg-tm work, and (c) any Defect you cause.
Section 2. Information about the Mission of Project Gutenberg-tm
Project Gutenberg-tm is synonymous with the free distribution of
electronic works in formats readable by the widest variety of computers
including obsolete, old, middle-aged and new computers. It exists
because of the efforts of hundreds of volunteers and donations from
people in all walks of life.
Volunteers and financial support to provide volunteers with the
assistance they need are critical to reaching Project Gutenberg-tm's
goals and ensuring that the Project Gutenberg-tm collection will
remain freely available for generations to come. In 2001, the Project
Gutenberg Literary Archive Foundation was created to provide a secure
and permanent future for Project Gutenberg-tm and future generations.
To learn more about the Project Gutenberg Literary Archive Foundation
and how your efforts and donations can help, see Sections 3 and 4
and the Foundation web page at http://www.pglaf.org.
Section 3. Information about the Project Gutenberg Literary Archive
Foundation
The Project Gutenberg Literary Archive Foundation is a non profit
501(c)(3) educational corporation organized under the laws of the
state of Mississippi and granted tax exempt status by the Internal
Revenue Service. The Foundation's EIN or federal tax identification
number is 64-6221541. Its 501(c)(3) letter is posted at
http://pglaf.org/fundraising. Contributions to the Project Gutenberg
Literary Archive Foundation are tax deductible to the full extent
permitted by U.S. federal laws and your state's laws.
The Foundation's principal office is located at 4557 Melan Dr. S.
Fairbanks, AK, 99712., but its volunteers and employees are scattered
throughout numerous locations. Its business office is located at
809 North 1500 West, Salt Lake City, UT 84116, (801) 596-1887, email
business@pglaf.org. Email contact links and up to date contact
information can be found at the Foundation's web site and official
page at http://pglaf.org
For additional contact information:
Dr. Gregory B. Newby
Chief Executive and Director
gbnewby@pglaf.org
Section 4. Information about Donations to the Project Gutenberg
Literary Archive Foundation
Project Gutenberg-tm depends upon and cannot survive without wide
spread public support and donations to carry out its mission of
increasing the number of public domain and licensed works that can be
freely distributed in machine readable form accessible by the widest
array of equipment including outdated equipment. Many small donations
($1 to $5,000) are particularly important to maintaining tax exempt
status with the IRS.
The Foundation is committed to complying with the laws regulating
charities and charitable donations in all 50 states of the United
States. Compliance requirements are not uniform and it takes a
considerable effort, much paperwork and many fees to meet and keep up
with these requirements. We do not solicit donations in locations
where we have not received written confirmation of compliance. To
SEND DONATIONS or determine the status of compliance for any
particular state visit http://pglaf.org
While we cannot and do not solicit contributions from states where we
have not met the solicitation requirements, we know of no prohibition
against accepting unsolicited donations from donors in such states who
approach us with offers to donate.
International donations are gratefully accepted, but we cannot make
any statements concerning tax treatment of donations received from
outside the United States. U.S. laws alone swamp our small staff.
Please check the Project Gutenberg Web pages for current donation
methods and addresses. Donations are accepted in a number of other
ways including including checks, online payments and credit card
donations. To donate, please visit: http://pglaf.org/donate
Section 5. General Information About Project Gutenberg-tm electronic
works.
Professor Michael S. Hart is the originator of the Project Gutenberg-tm
concept of a library of electronic works that could be freely shared
with anyone. For thirty years, he produced and distributed Project
Gutenberg-tm eBooks with only a loose network of volunteer support.
Project Gutenberg-tm eBooks are often created from several printed
editions, all of which are confirmed as Public Domain in the U.S.
unless a copyright notice is included. Thus, we do not necessarily
keep eBooks in compliance with any particular paper edition.
Most people start at our Web site which has the main PG search facility:
http://www.gutenberg.net
This Web site includes information about Project Gutenberg-tm,
including how to make donations to the Project Gutenberg Literary
Archive Foundation, how to help produce our new eBooks, and how to
subscribe to our email newsletter to hear about new eBooks.
================================================
FILE: packages/interface-ipfs-core/test/fixtures/test-folder/ipfs-add.js
================================================
#!/usr/bin/env node
'use strict'
const ipfs = require('../src')('localhost', 5001)
const files = process.argv.slice(2)
ipfs.add(files, { recursive: true }, function (err, res) {
if (err || !res) return console.log(err)
for (let i = 0; i < res.length; i++) {
console.log('added', res[i].Hash, res[i].Name)
}
})
================================================
FILE: packages/interface-ipfs-core/test/fixtures/test-folder/jungle.txt
================================================
Mowgli's Brothers
Now Rann the Kite brings home the night
That Mang the Bat sets free--
The herds are shut in byre and hut
For loosed till dawn are we.
This is the hour of pride and power,
Talon and tush and claw.
Oh, hear the call!--Good hunting all
That keep the Jungle Law!
Night-Song in the Jungle
It was seven o'clock of a very warm evening in the Seeonee hills when
Father Wolf woke up from his day's rest, scratched himself, yawned, and
spread out his paws one after the other to get rid of the sleepy feeling
in their tips. Mother Wolf lay with her big gray nose dropped across her
four tumbling, squealing cubs, and the moon shone into the mouth of the
cave where they all lived. "Augrh!" said Father Wolf. "It is time to
hunt again." He was going to spring down hill when a little shadow with
a bushy tail crossed the threshold and whined: "Good luck go with you, O
Chief of the Wolves. And good luck and strong white teeth go with noble
children that they may never forget the hungry in this world."
It was the jackal--Tabaqui, the Dish-licker--and the wolves of India
despise Tabaqui because he runs about making mischief, and telling
tales, and eating rags and pieces of leather from the village
rubbish-heaps. But they are afraid of him too, because Tabaqui, more
than anyone else in the jungle, is apt to go mad, and then he forgets
that he was ever afraid of anyone, and runs through the forest biting
everything in his way. Even the tiger runs and hides when little Tabaqui
goes mad, for madness is the most disgraceful thing that can overtake
a wild creature. We call it hydrophobia, but they call it dewanee--the
madness--and run.
"Enter, then, and look," said Father Wolf stiffly, "but there is no food
here."
"For a wolf, no," said Tabaqui, "but for so mean a person as myself a
dry bone is a good feast. Who are we, the Gidur-log [the jackal people],
to pick and choose?" He scuttled to the back of the cave, where he
found the bone of a buck with some meat on it, and sat cracking the end
merrily.
"All thanks for this good meal," he said, licking his lips. "How
beautiful are the noble children! How large are their eyes! And so young
too! Indeed, indeed, I might have remembered that the children of kings
================================================
FILE: packages/interface-ipfs-core/test/fixtures/test-folder/pp.txt
================================================
PRIDE AND PREJUDICE
By Jane Austen
Chapter 1
It is a truth universally acknowledged, that a single man in possession
of a good fortune, must be in want of a wife.
However little known the feelings or views of such a man may be on his
first entering a neighbourhood, this truth is so well fixed in the minds
of the surrounding families, that he is considered the rightful property
of some one or other of their daughters.
"My dear Mr. Bennet," said his lady to him one day, "have you heard that
Netherfield Park is let at last?"
Mr. Bennet replied that he had not.
"But it is," returned she; "for Mrs. Long has just been here, and she
told me all about it."
Mr. Bennet made no answer.
"Do you not want to know who has taken it?" cried his wife impatiently.
"_You_ want to tell me, and I have no objection to hearing it."
This was invitation enough.
"Why, my dear, you must know, Mrs. Long says that Netherfield is taken
by a young man of large fortune from the north of England; that he came
down on Monday in a chaise and four to see the place, and was so much
delighted with it, that he agreed with Mr. Morris immediately; that he
is to take possession before Michaelmas, and some of his servants are to
be in the house by the end of next week."
"What is his name?"
"Bingley."
"Is he married or single?"
"Oh! Single, my dear, to be sure! A single man of large fortune; four or
five thousand a year. What a fine thing for our girls!"
"How so? How can it affect them?"
"My dear Mr. Bennet," replied his wife, "how can you be so tiresome! You
must know that I am thinking of his marrying one of them."
"Is that his design in settling here?"
"Design! Nonsense, how can you talk so! But it is very likely that he
_may_ fall in love with one of them, and therefore you must visit him as
soon as he comes."
"I see no occasion for that. You and the girls may go, or you may send
them by themselves, which perhaps will be still better, for as you are
as handsome as any of them, Mr. Bingley may like you the best of the
party."
"My dear, you flatter me. I certainly _have_ had my share of beauty, but
I do not pretend to be anything extraordinary now. When a woman has five
grown-up daughters, she ought to give over thinking of her own beauty."
"In such cases, a woman has not often much beauty to think of."
"But, my dear, you must indeed go and see Mr. Bingley when he comes into
the neighbourhood."
"It is more than I engage for, I assure you."
"But consider your daughters. Only think what an establishment it would
be for one of them. Sir William and Lady Lucas are determined to
go, merely on that account, for in general, you know, they visit no
newcomers. Indeed you must go, for it will be impossible for _us_ to
visit him if you do not."
"You are over-scrupulous, surely. I dare say Mr. Bingley will be very
glad to see you; and I will send a few lines by you to assure him of my
hearty consent to his marrying whichever he chooses of the girls; though
I must throw in a good word for my little Lizzy."
"I desire you will do no such thing. Lizzy is not a bit better than the
others; and I am sure she is not half so handsome as Jane, nor half so
good-humoured as Lydia. But you are always giving _her_ the preference."
"They have none of them much to recommend them," replied he; "they are
all silly and ignorant like other girls; but Lizzy has something more of
quickness than her sisters."
"Mr. Bennet, how _can_ you abuse your own children in such a way? You
take delight in vexing me. You have no compassion for my poor nerves."
"You mistake me, my dear. I have a high respect for your nerves. They
are my old friends. I have heard you mention them with consideration
these last twenty years at least."
"Ah, you do not know what I suffer."
"But I hope you will get over it, and live to see many young men of four
thousand a year come into the neighbourhood."
"It will be no use to us, if twenty such should come, since you will not
visit them."
"Depend upon it, my dear, that when there are twenty, I will visit them
all."
Mr. Bennet was so odd a mixture of quick parts, sarcastic humour,
reserve, and caprice, that the experience of three-and-twenty years had
been insufficient to make his wife understand his character. _Her_ mind
was less difficult to develop. She was a woman of mean understanding,
little information, and uncertain temper. When she was discontented,
she fancied herself nervous. The business of her life was to get her
daughters married; its solace was visiting and news.
================================================
FILE: packages/interface-ipfs-core/test/fixtures/weird name folder [v0]/add
================================================
const ipfs = require('../src')('localhost', 5001)
const f1 = 'Hello'
const f2 = 'World'
ipfs.add([Uint8Array.from(f1), Uint8Array,from(f2)], function (err, res) {
if (err || !res) return console.log(err)
for (let i = 0; i < res.length; i++) {
console.log(res[i])
}
})
ipfs.add(['./files/hello.txt', './files/ipfs.txt'], function (err, res) {
if (err || !res) return console.log(err)
for (let i = 0; i < res.length; i++) {
console.log(res[i])
}
})
================================================
FILE: packages/interface-ipfs-core/test/fixtures/weird name folder [v0]/cat
================================================
const ipfs = require('../src')('localhost', 5001)
const hash = [
'QmdFyxZXsFiP4csgfM5uPu99AvFiKH62CSPDw5TP92nr7w',
'QmY9cxiHqTFoWamkQVkpmmqzBrY3hCBEL2XNu3NtX74Fuu'
]
ipfs.cat(hash, function (err, res) {
if (err || !res) return console.log(err)
if (res.readable) {
res.pipe(process.stdout)
} else {
console.log(res)
}
})
================================================
FILE: packages/interface-ipfs-core/test/fixtures/weird name folder [v0]/files/hello.txt
================================================
Hello
================================================
FILE: packages/interface-ipfs-core/test/fixtures/weird name folder [v0]/files/ipfs.txt
================================================
IPFS
================================================
FILE: packages/interface-ipfs-core/test/fixtures/weird name folder [v0]/hello-link
================================================
Hello
================================================
FILE: packages/interface-ipfs-core/test/fixtures/weird name folder [v0]/ipfs-add
================================================
#!/usr/bin/env node
const ipfs = require('../src')('localhost', 5001)
const files = process.argv.slice(2)
ipfs.add(files, {recursive: true}, function (err, res) {
if (err || !res) return console.log(err)
for (let i = 0; i < res.length; i++) {
console.log('added', res[i].Hash, res[i].Name)
}
})
================================================
FILE: packages/interface-ipfs-core/test/fixtures/weird name folder [v0]/ls
================================================
const ipfs = require('../src')('localhost', 5001)
const hash = ['QmdbHK6gMiecyjjSoPnfJg6iKMF7v6E2NkoBgGpmyCoevh']
ipfs.ls(hash, function (err, res) {
if (err || !res) return console.log(err)
res.Objects.forEach(function (node) {
console.log(node.Hash)
console.log('Links [%d]', node.Links.length)
node.Links.forEach(function (link, i) {
console.log('[%d]', i, link)
})
})
})
================================================
FILE: packages/interface-ipfs-core/test/fixtures/weird name folder [v0]/version
================================================
const ipfs = require('../src')('localhost', 5001)
ipfs.commands(function (err, res) {
if (err) throw err
console.log(res)
})
================================================
FILE: packages/interface-ipfs-core/test/interface.spec.js
================================================
================================================
FILE: packages/interface-ipfs-core/tsconfig.json
================================================
{
"extends": "aegir/src/config/tsconfig.aegir.json",
"compilerOptions": {
"outDir": "dist",
"emitDeclarationOnly": true
},
"include": [
"src",
"test"
],
"exclude": [
"test/fixtures/*"
],
"references": [
{
"path": "../ipfs-core-types"
}
]
}
================================================
FILE: packages/ipfs/.aegir.js
================================================
import getPort from 'aegir/get-port'
import { createServer } from 'ipfsd-ctl'
import EchoServer from 'aegir/echo-server'
import { sigServer } from '@libp2p/webrtc-star-signalling-server'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
/** @type {import('aegir').Options["build"]["config"]} */
const esbuild = {
inject: [path.join(__dirname, '../../scripts/node-globals.js')]
}
/** @type {import('aegir').PartialOptions} */
export default {
test: {
browser: {
config: {
assets: '..',
buildConfig: esbuild
}
},
before: async (options) => {
const MockPreloadNode = await import('./test/utils/mock-preload-node.js')
const { PinningService } = await import('./test/utils/mock-pinning-service.js')
const echoServer = new EchoServer()
const preloadNode = MockPreloadNode.createNode()
const pinningService = await PinningService.start()
await preloadNode.start()
await echoServer.start()
if (options.runner !== 'node') {
const ipfsClient = await import('ipfs-client')
const ipfsdPort = await getPort()
const signalAPort = await getPort()
const signalBPort = await getPort()
const sigServerA = await sigServer({
host: '127.0.0.1',
port: signalAPort,
metrics: false
})
// the second signalling server is needed for the interface test 'should list peers only once even if they have multiple addresses'
const sigServerB = await sigServer({
host: '127.0.0.1',
port: signalBPort,
metrics: false
})
const ipfsdServer = await createServer({
host: '127.0.0.1',
port: ipfsdPort
}, {
type: 'js',
ipfsModule: await import(path.join(__dirname, 'src', 'index.js')),
ipfsHttpModule: await import('ipfs-http-client'),
ipfsBin: path.join(__dirname, 'src', 'cli.js'),
ipfsOptions: {
libp2p: {
dialer: {
dialTimeout: 60e3 // increase timeout because travis is slow
}
}
}
}, {
go: {
ipfsBin: (await import('go-ipfs')).default.path()
},
js: {
ipfsClientModule: {
create: ipfsClient.create
}
}
}).start()
return {
env: {
PINNING_SERVICE_ENDPOINT: pinningService.endpoint,
PINNING_SERVICE_KEY: pinningService.token,
ECHO_SERVER: `http://${echoServer.host}:${echoServer.port}`,
IPFSD_SERVER: `http://127.0.0.1:${ipfsdPort}`,
SIGNALA_SERVER: `/ip4/127.0.0.1/tcp/${signalAPort}/ws/p2p-webrtc-star`,
SIGNALB_SERVER: `/ip4/127.0.0.1/tcp/${signalBPort}/ws/p2p-webrtc-star`
},
echoServer,
preloadNode,
pinningService,
ipfsdServer,
sigServerA,
sigServerB
}
}
return {
env: {
PINNING_SERVICE_ENDPOINT: pinningService.endpoint,
PINNING_SERVICE_KEY: pinningService.token,
ECHO_SERVER: `http://${echoServer.host}:${echoServer.port}`
},
echoServer,
preloadNode,
pinningService
}
},
after: async (options, beforeResult) => {
const { PinningService } = await import('./test/utils/mock-pinning-service.js')
await beforeResult.echoServer.stop()
await beforeResult.preloadNode.stop()
await PinningService.stop(beforeResult.pinningService)
if (options.runner !== 'node') {
await beforeResult.ipfsdServer.stop()
await beforeResult.sigServerA.stop()
await beforeResult.sigServerB.stop()
}
}
},
build: {
bundlesizeMax: '477KB',
config: esbuild
},
dependencyCheck: {
ignore: [
'assert',
'cross-env',
'rimraf',
'url',
'wrtc',
'electron-webrtc',
'ipfs-interop'
]
}
}
================================================
FILE: packages/ipfs/CHANGELOG.md
================================================
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
### [0.66.1](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.66.0...ipfs-v0.66.1) (2023-05-25)
### Bug Fixes
* add deprecation notice to readmes ([#4362](https://www.github.com/ipfs/js-ipfs/issues/4362)) ([7b79c1b](https://www.github.com/ipfs/js-ipfs/commit/7b79c1b8df5c818dc124b346ea28330455732d5c))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.16.0 to ^0.16.1
* ipfs-core bumped from ^0.18.0 to ^0.18.1
* devDependencies
* interface-ipfs-core bumped from ^0.158.0 to ^0.158.1
* ipfs-client bumped from ^0.10.0 to ^0.10.1
* ipfs-core-types bumped from ^0.14.0 to ^0.14.1
* ipfs-http-client bumped from ^60.0.0 to ^60.0.1
## [0.66.0](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.65.0...ipfs-v0.66.0) (2023-01-11)
### ⚠ BREAKING CHANGES
* update multiformats to v11.x.x and related depenendcies (#4277)
### Bug Fixes
* update multiformats to v11.x.x and related depenendcies ([#4277](https://www.github.com/ipfs/js-ipfs/issues/4277)) ([521c84a](https://www.github.com/ipfs/js-ipfs/commit/521c84a958b04d61702577a5adce28519c1b2a3b))
* use aegir to publish RCs ([#4284](https://www.github.com/ipfs/js-ipfs/issues/4284)) ([6d90cbf](https://www.github.com/ipfs/js-ipfs/commit/6d90cbf321a7dbf4b1084ba20f0c514dc08d8d0a))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.15.0 to ^0.16.0
* ipfs-core bumped from ^0.17.0 to ^0.18.0
* devDependencies
* interface-ipfs-core bumped from ^0.157.0 to ^0.158.0
* ipfs-client bumped from ^0.9.2 to ^0.10.0
* ipfs-core-types bumped from ^0.13.0 to ^0.14.0
* ipfs-http-client bumped from ^59.0.0 to ^60.0.0
## [0.65.0](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.64.2...ipfs-v0.65.0) (2022-10-24)
### ⚠ BREAKING CHANGES
* ipfs is now bundled with libp2p@0.40.x which has different config
### Features
* upgrade libp2p to 0.40.x ([#4237](https://www.github.com/ipfs/js-ipfs/issues/4237)) ([0cee4a4](https://www.github.com/ipfs/js-ipfs/commit/0cee4a4c55767022584dcbade0b0b9b43326f9c9))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.14.2 to ^0.15.0
* ipfs-core bumped from ^0.16.1 to ^0.17.0
* devDependencies
* interface-ipfs-core bumped from ^0.156.1 to ^0.157.0
* ipfs-client bumped from ^0.9.1 to ^0.9.2
* ipfs-core-types bumped from ^0.12.1 to ^0.13.0
* ipfs-http-client bumped from ^58.0.1 to ^59.0.0
### [0.64.2](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.64.1...ipfs-v0.64.2) (2022-09-21)
### Bug Fixes
* update @multiformats/multiadd to 11.0.0 ([2a830bf](https://www.github.com/ipfs/js-ipfs/commit/2a830bf58a5929fcce51dede871c99f62192fbda))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.14.1 to ^0.14.2
* ipfs-core bumped from ^0.16.0 to ^0.16.1
* devDependencies
* interface-ipfs-core bumped from ^0.156.0 to ^0.156.1
* ipfs-client bumped from ^0.9.0 to ^0.9.1
* ipfs-core-types bumped from ^0.12.0 to ^0.12.1
* ipfs-http-client bumped from ^58.0.0 to ^58.0.1
### [0.64.1](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.64.0...ipfs-v0.64.1) (2022-09-16)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.14.0 to ^0.14.1
## [0.64.0](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.63.5...ipfs-v0.64.0) (2022-09-06)
### ⚠ BREAKING CHANGES
* update to libp2p@0.38.x (#4151)
### deps
* update to libp2p@0.38.x ([#4151](https://www.github.com/ipfs/js-ipfs/issues/4151)) ([39dbf70](https://www.github.com/ipfs/js-ipfs/commit/39dbf708ec31b263115e44f420651fa4e056a89e))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.13.0 to ^0.14.0
* ipfs-core bumped from ^0.15.0 to ^0.16.0
* devDependencies
* interface-ipfs-core bumped from ^0.155.0 to ^0.156.0
* ipfs-client bumped from ^0.8.0 to ^0.9.0
* ipfs-core-types bumped from ^0.11.0 to ^0.12.0
* ipfs-http-client bumped from ^57.0.0 to ^58.0.0
### [0.63.5](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.63.4...ipfs-v0.63.5) (2022-06-24)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.13.4 to ^0.13.5
* ipfs-core bumped from ^0.15.3 to ^0.15.4
* devDependencies
* interface-ipfs-core bumped from ^0.155.1 to ^0.155.2
* ipfs-client bumped from ^0.8.2 to ^0.8.3
* ipfs-http-client bumped from ^57.0.2 to ^57.0.3
### [0.63.4](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.63.3...ipfs-v0.63.4) (2022-06-22)
### Bug Fixes
* use default ws filters instead of connecting to everything ([#4142](https://www.github.com/ipfs/js-ipfs/issues/4142)) ([7be50bd](https://www.github.com/ipfs/js-ipfs/commit/7be50bd157b984d4607545bb78d22cd33de933fa)), closes [#4141](https://www.github.com/ipfs/js-ipfs/issues/4141)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.13.3 to ^0.13.4
* ipfs-core bumped from ^0.15.2 to ^0.15.3
* devDependencies
* interface-ipfs-core bumped from ^0.155.0 to ^0.155.1
* ipfs-client bumped from ^0.8.1 to ^0.8.2
* ipfs-core-types bumped from ^0.11.0 to ^0.11.1
* ipfs-http-client bumped from ^57.0.1 to ^57.0.2
### [0.63.3](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.63.2...ipfs-v0.63.3) (2022-06-13)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.13.2 to ^0.13.3
* ipfs-core bumped from ^0.15.1 to ^0.15.2
## [0.63.2](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.63.1...ipfs-v0.63.2) (2022-06-01)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.13.1 to ^0.13.2
* ipfs-core bumped from ^0.15.0 to ^0.15.1
* devDependencies
* ipfs-client bumped from ^0.8.0 to ^0.8.1
* ipfs-http-client bumped from ^57.0.0 to ^57.0.1
## [0.63.1](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.63.0...ipfs-v0.63.1) (2022-05-30)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.13.0 to ^0.13.1
## [0.63.0](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.62.3...ipfs-v0.63.0) (2022-05-27)
### ⚠ BREAKING CHANGES
* This module is now ESM only and there return types of some methods have changed. See the [upgrade guide](https://github.com/ipfs/js-ipfs/blob/master/docs/upgrading/v0.62-v0.63.md) for more info.
### Features
* update to libp2p 0.37.x ([#4092](https://www.github.com/ipfs/js-ipfs/issues/4092)) ([74aee8b](https://www.github.com/ipfs/js-ipfs/commit/74aee8b3d78f233c3199a3e9a6c0ac628a31a433))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.12.3 to ^0.13.0
* ipfs-core bumped from ^0.14.3 to ^0.15.0
* devDependencies
* interface-ipfs-core bumped from ^0.154.2 to ^0.155.0
* ipfs-client bumped from ^0.7.8 to ^0.8.0
* ipfs-core-types bumped from ^0.10.3 to ^0.11.0
* ipfs-http-client bumped from ^56.0.3 to ^57.0.0
### [0.62.3](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.62.2...ipfs-v0.62.3) (2022-04-20)
### Bug Fixes
* exclude fs from bundle ([#4076](https://www.github.com/ipfs/js-ipfs/issues/4076)) ([6c3cb73](https://www.github.com/ipfs/js-ipfs/commit/6c3cb73db7b46211c88431273f61f04463a4f80d))
* upgrade dep of ipfs-utils ^9.0.2->^9.0.6 ([#4086](https://www.github.com/ipfs/js-ipfs/issues/4086)) ([8f7ce23](https://www.github.com/ipfs/js-ipfs/commit/8f7ce23c18be12bdc52b98bfccbd0a5a2a9c9f7e)), closes [#4080](https://www.github.com/ipfs/js-ipfs/issues/4080)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.12.2 to ^0.12.3
* ipfs-core bumped from ^0.14.2 to ^0.14.3
* devDependencies
* interface-ipfs-core bumped from ^0.154.2 to ^0.154.3
* ipfs-client bumped from ^0.7.8 to ^0.7.9
* ipfs-core-types bumped from ^0.10.2 to ^0.10.3
* ipfs-http-client bumped from ^56.0.2 to ^56.0.3
### [0.62.2](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.62.1...ipfs-v0.62.2) (2022-03-01)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.12.1 to ^0.12.2
* ipfs-core bumped from ^0.14.1 to ^0.14.2
* devDependencies
* interface-ipfs-core bumped from ^0.154.1 to ^0.154.2
* ipfs-client bumped from ^0.7.7 to ^0.7.8
* ipfs-core-types bumped from ^0.10.1 to ^0.10.2
* ipfs-http-client bumped from ^56.0.1 to ^56.0.2
### [0.62.1](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.62.0...ipfs-v0.62.1) (2022-02-06)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.12.0 to ^0.12.1
* ipfs-core bumped from ^0.14.0 to ^0.14.1
* devDependencies
* interface-ipfs-core bumped from ^0.154.0 to ^0.154.1
* ipfs-client bumped from ^0.7.6 to ^0.7.7
* ipfs-core-types bumped from ^0.10.0 to ^0.10.1
* ipfs-http-client bumped from ^56.0.0 to ^56.0.1
## [0.62.0](https://www.github.com/ipfs/js-ipfs/compare/ipfs-v0.61.0...ipfs-v0.62.0) (2022-01-27)
### ⚠ BREAKING CHANGES
* peerstore methods are now all async, the repo is migrated to v12
### Features
* libp2p async peerstore ([#4018](https://www.github.com/ipfs/js-ipfs/issues/4018)) ([a6b201a](https://www.github.com/ipfs/js-ipfs/commit/a6b201af2c3697430ab0ebe002dd573d185f1ac0))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-cli bumped from ^0.11.0 to ^0.12.0
* ipfs-core bumped from ^0.13.0 to ^0.14.0
* devDependencies
* interface-ipfs-core bumped from ^0.153.0 to ^0.154.0
* ipfs-client bumped from ^0.7.5 to ^0.7.6
* ipfs-core-types bumped from ^0.9.0 to ^0.10.0
* ipfs-http-client bumped from ^55.0.0 to ^56.0.0
## [0.61.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.60.2...ipfs@0.61.0) (2021-12-15)
### Bug Fixes
* **pubsub:** multibase in pubsub http rpc ([#3922](https://github.com/ipfs/js-ipfs/issues/3922)) ([6eeaca4](https://github.com/ipfs/js-ipfs/commit/6eeaca452c36fa13be42d704575c577e4ca938f1))
### Features
* dht client ([#3947](https://github.com/ipfs/js-ipfs/issues/3947)) ([62d8ecb](https://github.com/ipfs/js-ipfs/commit/62d8ecbc723e693a2544e69172d99c576d187c23))
* update DAG API to match go-ipfs@0.10 changes ([#3917](https://github.com/ipfs/js-ipfs/issues/3917)) ([38c01be](https://github.com/ipfs/js-ipfs/commit/38c01be03b4fd5f401cd9b698cfdb4237d835b01))
### BREAKING CHANGES
* **pubsub:** We had to make breaking changes to `pubsub` commands sent over HTTP RPC to fix data corruption caused by topic names and payload bytes that included `\n`. More details in https://github.com/ipfs/go-ipfs/issues/7939 and https://github.com/ipfs/go-ipfs/pull/8183
* `ipfs.dag.put` no longer accepts a `format` arg, it is now `storeCodec` and `inputCodec`. `'json'` has become `'dag-json'`, `'cbor'` has become `'dag-cbor'` and so on
* The DHT API has been refactored to return async iterators of query events
## [0.60.2](https://github.com/ipfs/js-ipfs/compare/ipfs@0.60.1...ipfs@0.60.2) (2021-11-24)
**Note:** Version bump only for package ipfs
## [0.60.1](https://github.com/ipfs/js-ipfs/compare/ipfs@0.60.0...ipfs@0.60.1) (2021-11-19)
**Note:** Version bump only for package ipfs
## [0.60.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.59.1...ipfs@0.60.0) (2021-11-12)
### Bug Fixes
* do not accept single items for ipfs.add ([#3900](https://github.com/ipfs/js-ipfs/issues/3900)) ([04e3cf3](https://github.com/ipfs/js-ipfs/commit/04e3cf3f46b585c4644cba70516f375e95361f52))
### BREAKING CHANGES
* errors will now be thrown if multiple items are passed to `ipfs.add` or single items to `ipfs.addAll` (n.b. you can still pass a list of a single item to `ipfs.addAll`)
## [0.59.1](https://github.com/ipfs/js-ipfs/compare/ipfs@0.59.0...ipfs@0.59.1) (2021-09-28)
**Note:** Version bump only for package ipfs
## [0.59.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.58.6...ipfs@0.59.0) (2021-09-24)
### Features
* pull in new globSource ([#3889](https://github.com/ipfs/js-ipfs/issues/3889)) ([be4a542](https://github.com/ipfs/js-ipfs/commit/be4a5428ebc4b05a2edd9a91bf9df6416c1a8c2b))
* switch to esm ([#3879](https://github.com/ipfs/js-ipfs/issues/3879)) ([9a40109](https://github.com/ipfs/js-ipfs/commit/9a40109632e5b4837eb77a2f57dbc77fbf1fe099))
### BREAKING CHANGES
* the globSource api has changed from `globSource(dir, opts)` to `globSource(dir, pattern, opts)`
* There are no default exports and everything is now dual published as ESM/CJS
## [0.58.6](https://github.com/ipfs/js-ipfs/compare/ipfs@0.58.5...ipfs@0.58.6) (2021-09-17)
**Note:** Version bump only for package ipfs
## [0.58.5](https://github.com/ipfs/js-ipfs/compare/ipfs@0.58.4...ipfs@0.58.5) (2021-09-17)
**Note:** Version bump only for package ipfs
## [0.58.4](https://github.com/ipfs/js-ipfs/compare/ipfs@0.58.3...ipfs@0.58.4) (2021-09-08)
**Note:** Version bump only for package ipfs
## [0.58.3](https://github.com/ipfs/js-ipfs/compare/ipfs@0.58.2...ipfs@0.58.3) (2021-09-02)
### Bug Fixes
* remove use of instanceof for CID class ([#3847](https://github.com/ipfs/js-ipfs/issues/3847)) ([ebbb12d](https://github.com/ipfs/js-ipfs/commit/ebbb12db523c53ce8e4ddae5266cd9acb3504431))
## [0.58.2](https://github.com/ipfs/js-ipfs/compare/ipfs@0.58.1...ipfs@0.58.2) (2021-08-25)
**Note:** Version bump only for package ipfs
## [0.58.1](https://github.com/ipfs/js-ipfs/compare/ipfs@0.58.0...ipfs@0.58.1) (2021-08-17)
**Note:** Version bump only for package ipfs
## [0.58.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.57.0...ipfs@0.58.0) (2021-08-17)
### Features
* pubsub over gRPC ([#3813](https://github.com/ipfs/js-ipfs/issues/3813)) ([e7d5509](https://github.com/ipfs/js-ipfs/commit/e7d5509c87e87aed6be3c1d0b2a01ab74cdc1ed9)), closes [#3741](https://github.com/ipfs/js-ipfs/issues/3741)
## [0.57.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.56.1...ipfs@0.57.0) (2021-08-11)
### Features
* make ipfs.get output tarballs ([#3785](https://github.com/ipfs/js-ipfs/issues/3785)) ([1ad6001](https://github.com/ipfs/js-ipfs/commit/1ad60018d39d5b46c484756631e30e1989fd8eba))
### BREAKING CHANGES
* the output type of `ipfs.get` has changed and the `recursive` option has been removed from `ipfs.ls` since it was not supported everywhere
## [0.56.1](https://github.com/ipfs/js-ipfs/compare/ipfs@0.56.0...ipfs@0.56.1) (2021-07-30)
**Note:** Version bump only for package ipfs
## [0.56.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.55.4...ipfs@0.56.0) (2021-07-27)
### Features
* upgrade to the new multiformats ([#3556](https://github.com/ipfs/js-ipfs/issues/3556)) ([d13d15f](https://github.com/ipfs/js-ipfs/commit/d13d15f022a87d04a35f0f7822142f9cb898479c))
### BREAKING CHANGES
* ipld-formats no longer supported, use multiformat BlockCodecs instead
Co-authored-by: Rod Vagg
Co-authored-by: achingbrain
## [0.55.4](https://github.com/ipfs/js-ipfs/compare/ipfs@0.55.3...ipfs@0.55.4) (2021-06-18)
**Note:** Version bump only for package ipfs
## [0.55.3](https://github.com/ipfs/js-ipfs/compare/ipfs@0.55.2...ipfs@0.55.3) (2021-06-05)
### Bug Fixes
* stalling subscription on (node) http-client when daemon is stopped ([#3468](https://github.com/ipfs/js-ipfs/issues/3468)) ([0266abf](https://github.com/ipfs/js-ipfs/commit/0266abf0c4b817636172f78c6e91eb4dd5aad451)), closes [#3465](https://github.com/ipfs/js-ipfs/issues/3465)
## [0.55.2](https://github.com/ipfs/js-ipfs/compare/ipfs@0.55.1...ipfs@0.55.2) (2021-05-26)
**Note:** Version bump only for package ipfs
## [0.55.1](https://github.com/ipfs/js-ipfs/compare/ipfs@0.55.0...ipfs@0.55.1) (2021-05-11)
**Note:** Version bump only for package ipfs
## [0.55.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.54.4...ipfs@0.55.0) (2021-05-10)
### Bug Fixes
* mark ipld options as partial ([#3669](https://github.com/ipfs/js-ipfs/issues/3669)) ([f98af8e](https://github.com/ipfs/js-ipfs/commit/f98af8ed24784929898bb5d33a64dc442c77074d))
* only accept cid for ipfs.dag.get ([#3675](https://github.com/ipfs/js-ipfs/issues/3675)) ([bb8f8bc](https://github.com/ipfs/js-ipfs/commit/bb8f8bc501ffc1ee0f064ba61ec0bca4015bf6ad)), closes [#3637](https://github.com/ipfs/js-ipfs/issues/3637)
### chore
* update node version in docker build ([#3603](https://github.com/ipfs/js-ipfs/issues/3603)) ([087fd1e](https://github.com/ipfs/js-ipfs/commit/087fd1eb402d1b933730e09c1d0cfb21067e9992))
* upgrade deps with new typedefs ([#3550](https://github.com/ipfs/js-ipfs/issues/3550)) ([a418a52](https://github.com/ipfs/js-ipfs/commit/a418a521574c878d7aabd0ad2fd8d516908a3756))
### BREAKING CHANGES
* Minimum supported node version is 14
* all core api methods now have types, some method signatures have changed, named exports are now used by the http, grpc and ipfs client modules
## [0.54.4](https://github.com/ipfs/js-ipfs/compare/ipfs@0.54.3...ipfs@0.54.4) (2021-03-10)
**Note:** Version bump only for package ipfs
## [0.54.3](https://github.com/ipfs/js-ipfs/compare/ipfs@0.54.2...ipfs@0.54.3) (2021-03-09)
### Bug Fixes
* update to new aegir ([#3528](https://github.com/ipfs/js-ipfs/issues/3528)) ([49f7880](https://github.com/ipfs/js-ipfs/commit/49f78807d7e26483bd926b45cc7e0f797d77e41b))
## [0.54.2](https://github.com/ipfs/js-ipfs/compare/ipfs@0.54.1...ipfs@0.54.2) (2021-02-08)
**Note:** Version bump only for package ipfs
## [0.54.1](https://github.com/ipfs/js-ipfs/compare/ipfs@0.54.0...ipfs@0.54.1) (2021-02-02)
**Note:** Version bump only for package ipfs
## [0.54.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.53.2...ipfs@0.54.0) (2021-02-01)
### Bug Fixes
* updates webpack example to use v5 ([#3512](https://github.com/ipfs/js-ipfs/issues/3512)) ([c7110db](https://github.com/ipfs/js-ipfs/commit/c7110db71b5c0f0f9f415f31f91b5b228341e13e)), closes [#3511](https://github.com/ipfs/js-ipfs/issues/3511)
### chore
* update deps ([#3514](https://github.com/ipfs/js-ipfs/issues/3514)) ([061d77c](https://github.com/ipfs/js-ipfs/commit/061d77cc03f40af5a3bc3590481e1e5836e7f0d8))
### Features
* support remote pinning services in ipfs-http-client ([#3293](https://github.com/ipfs/js-ipfs/issues/3293)) ([ba240fd](https://github.com/ipfs/js-ipfs/commit/ba240fdf93edc88028315483240d7822a7ca88ed))
### BREAKING CHANGES
* ipfs-repo upgrade requires repo migration to v10
## [0.53.2](https://github.com/ipfs/js-ipfs/compare/ipfs@0.53.1...ipfs@0.53.2) (2021-01-22)
**Note:** Version bump only for package ipfs
## [0.53.1](https://github.com/ipfs/js-ipfs/compare/ipfs@0.53.0...ipfs@0.53.1) (2021-01-20)
**Note:** Version bump only for package ipfs
## [0.53.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.52.3...ipfs@0.53.0) (2021-01-15)
### chore
* update libp2p to 0.30 ([#3427](https://github.com/ipfs/js-ipfs/issues/3427)) ([a39e6fb](https://github.com/ipfs/js-ipfs/commit/a39e6fb372bf9e7782462b6a4b7530a3f8c9b3f1))
### Features
* add grpc server and client ([#3403](https://github.com/ipfs/js-ipfs/issues/3403)) ([a9027e0](https://github.com/ipfs/js-ipfs/commit/a9027e0ec0cea9a4f34b4f2f52e09abb35237384)), closes [#2519](https://github.com/ipfs/js-ipfs/issues/2519) [#2838](https://github.com/ipfs/js-ipfs/issues/2838) [#2943](https://github.com/ipfs/js-ipfs/issues/2943) [#2854](https://github.com/ipfs/js-ipfs/issues/2854) [#2864](https://github.com/ipfs/js-ipfs/issues/2864)
### BREAKING CHANGES
* The websocket transport will only dial DNS+WSS addresses - see https://github.com/libp2p/js-libp2p-websockets/releases/tag/v0.15.0
Co-authored-by: Hugo Dias
## [0.52.3](https://github.com/ipfs/js-ipfs/compare/ipfs@0.52.2...ipfs@0.52.3) (2020-12-16)
### Bug Fixes
* export IPFS type ([#3447](https://github.com/ipfs/js-ipfs/issues/3447)) ([cacbfc6](https://github.com/ipfs/js-ipfs/commit/cacbfc6e87eabee0e2a6df2056ac5cc993690a0d)), closes [#3439](https://github.com/ipfs/js-ipfs/issues/3439)
* fix ipfs.ls() for a single file object ([#3440](https://github.com/ipfs/js-ipfs/issues/3440)) ([f243dd1](https://github.com/ipfs/js-ipfs/commit/f243dd1c37fcb9786d77d129cd9b238457d18a15))
## [0.52.2](https://github.com/ipfs/js-ipfs/compare/ipfs@0.52.1...ipfs@0.52.2) (2020-11-25)
**Note:** Version bump only for package ipfs
## [0.52.1](https://github.com/ipfs/js-ipfs/compare/ipfs@0.52.0...ipfs@0.52.1) (2020-11-16)
### Bug Fixes
* report ipfs.add progress over http ([#3310](https://github.com/ipfs/js-ipfs/issues/3310)) ([39cad4b](https://github.com/ipfs/js-ipfs/commit/39cad4b76b950ea6a76477fd01f8631b8bd9aa1e))
## [0.52.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.51.0...ipfs@0.52.0) (2020-11-09)
### Bug Fixes
* typedef resolution & add examples that use types ([#3359](https://github.com/ipfs/js-ipfs/issues/3359)) ([dc2795a](https://github.com/ipfs/js-ipfs/commit/dc2795a4f3b515683d09967ce611bf87d5e67f86)), closes [#3356](https://github.com/ipfs/js-ipfs/issues/3356) [#3358](https://github.com/ipfs/js-ipfs/issues/3358)
### Features
* remove all esoteric ipld formats ([#3360](https://github.com/ipfs/js-ipfs/issues/3360)) ([a542882](https://github.com/ipfs/js-ipfs/commit/a5428820a5b157fbb298b8eb49978e08157beca3)), closes [#3347](https://github.com/ipfs/js-ipfs/issues/3347)
### BREAKING CHANGES
* only dag-pb, dag-cbor and raw formats are supported out of the box, any others will need to be configured during node startup.
## [0.51.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.50.2...ipfs@0.51.0) (2020-10-28)
### Bug Fixes
* disable cors by default ([#3275](https://github.com/ipfs/js-ipfs/issues/3275)) ([3ff833d](https://github.com/ipfs/js-ipfs/commit/3ff833db6444a3e931db9b76bf74c3420e57ee02))
* types path for ipfs-core ([#3356](https://github.com/ipfs/js-ipfs/issues/3356)) ([a6bcad5](https://github.com/ipfs/js-ipfs/commit/a6bcad5d9e63a74897715e6bf66ff213424faa66))
* use fetch in electron renderer and electron-fetch in main ([#3251](https://github.com/ipfs/js-ipfs/issues/3251)) ([639d71f](https://github.com/ipfs/js-ipfs/commit/639d71f7ac8f66d9633e753a2a6be927e14a5af0))
### Features
* remove support for SECIO ([#3295](https://github.com/ipfs/js-ipfs/issues/3295)) ([5f5ef7e](https://github.com/ipfs/js-ipfs/commit/5f5ef7ee6cc6dc634cc6adbede0602492490a85d))
* type check & generate defs from jsdoc ([#3281](https://github.com/ipfs/js-ipfs/issues/3281)) ([bbcaf34](https://github.com/ipfs/js-ipfs/commit/bbcaf34111251b142273a5675f4754ff68bd9fa0))
### BREAKING CHANGES
* this removes support for SECIO making Noise the only security transport.
Closes https://github.com/ipfs/js-ipfs/issues/3210
Co-authored-by: achingbrain
* - CORS origins will need to be [configured manually](https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs-http-client/README.md#cors) before use with ipfs-http-client
## [0.50.2](https://github.com/ipfs/js-ipfs/compare/ipfs@0.50.1...ipfs@0.50.2) (2020-09-09)
**Note:** Version bump only for package ipfs
## [0.50.1](https://github.com/ipfs/js-ipfs/compare/ipfs@0.50.0...ipfs@0.50.1) (2020-09-04)
**Note:** Version bump only for package ipfs
## [0.50.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.49.1...ipfs@0.50.0) (2020-09-03)
### Features
* add protocol list to ipfs id ([#3250](https://github.com/ipfs/js-ipfs/issues/3250)) ([1b6cf60](https://github.com/ipfs/js-ipfs/commit/1b6cf600a6b1348199457ca1fe6f314b6eff8c46))
* add typeScript support ([#3236](https://github.com/ipfs/js-ipfs/issues/3236)) ([be26dd7](https://github.com/ipfs/js-ipfs/commit/be26dd723ed8c76efee149a993a8ade7f75f960e)), closes [#2945](https://github.com/ipfs/js-ipfs/issues/2945) [#1166](https://github.com/ipfs/js-ipfs/issues/1166)
* add typescript support ([#3267](https://github.com/ipfs/js-ipfs/issues/3267)) ([6816bc6](https://github.com/ipfs/js-ipfs/commit/6816bc64ccb9bf852c2b9a26d9ddd19b9439dae6)), closes [#2945](https://github.com/ipfs/js-ipfs/issues/2945) [#1166](https://github.com/ipfs/js-ipfs/issues/1166)
* ipns publish example ([#3207](https://github.com/ipfs/js-ipfs/issues/3207)) ([91faec6](https://github.com/ipfs/js-ipfs/commit/91faec6e3d89b0d9883b8d7815c276d44048e739))
* store pins in datastore instead of a DAG ([#2771](https://github.com/ipfs/js-ipfs/issues/2771)) ([64b7fe4](https://github.com/ipfs/js-ipfs/commit/64b7fe41738cbe96d5a9075f0c01156c6f889c40))
* update hapi to v20 ([#3245](https://github.com/ipfs/js-ipfs/issues/3245)) ([1aeef89](https://github.com/ipfs/js-ipfs/commit/1aeef89c73f42a2f6cceb7f0598400141ce40e23))
* update to libp2p@0.29.0 ([63d4d35](https://github.com/ipfs/js-ipfs/commit/63d4d353c606e4fd487811d8a0014bb2173f11be))
## [0.49.1](https://github.com/ipfs/js-ipfs/compare/ipfs@0.49.0...ipfs@0.49.1) (2020-08-24)
### Bug Fixes
* validate ipns records with inline public keys ([#3224](https://github.com/ipfs/js-ipfs/issues/3224)) ([5cc0e08](https://github.com/ipfs/js-ipfs/commit/5cc0e086b036e7ba40b09768b67b7067adca43c1))
## [0.49.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.48.1...ipfs@0.49.0) (2020-08-12)
### Bug Fixes
* make execa a dep, it's used in ipfs config edit ([#3193](https://github.com/ipfs/js-ipfs/issues/3193)) ([19b8113](https://github.com/ipfs/js-ipfs/commit/19b81130a7311744cdd6b5bc2170d3939aeae1b6))
* require command for key and pin subcommands ([#3196](https://github.com/ipfs/js-ipfs/issues/3196)) ([5449044](https://github.com/ipfs/js-ipfs/commit/5449044919b8440c1129d9cbf1ec650f4f5a993d))
* send blobs when running ipfs-http-client in the browser ([#3184](https://github.com/ipfs/js-ipfs/issues/3184)) ([6b24463](https://github.com/ipfs/js-ipfs/commit/6b24463431497bd13b579a730ad7063345729ad9)), closes [#3138](https://github.com/ipfs/js-ipfs/issues/3138)
* support keychain without pass ([#3212](https://github.com/ipfs/js-ipfs/issues/3212)) ([7e0e85c](https://github.com/ipfs/js-ipfs/commit/7e0e85c2f003a09845b1dbe4200ca61366933b05))
* **docs:** update webrtc config example to use correct case ([6a498e9](https://github.com/ipfs/js-ipfs/commit/6a498e92c00a784867053cddf9dcf4c1f510cf55))
* **docs:** update webrtc instructions for node in faq ([#3183](https://github.com/ipfs/js-ipfs/issues/3183)) ([8f5a19f](https://github.com/ipfs/js-ipfs/commit/8f5a19ff08023e22fb3c4ab9dcac1e7baa097d09))
### Features
* prioritize noise over secio ([#3216](https://github.com/ipfs/js-ipfs/issues/3216)) ([f3a67c4](https://github.com/ipfs/js-ipfs/commit/f3a67c43c3d3423df29b5e10f82fa483d31289b2))
* share IPFS node between browser tabs ([#3081](https://github.com/ipfs/js-ipfs/issues/3081)) ([1b8b1b8](https://github.com/ipfs/js-ipfs/commit/1b8b1b822a252498889c54972a1f57e1fedc39d0)), closes [#3022](https://github.com/ipfs/js-ipfs/issues/3022)
### BREAKING CHANGES
* remove support for key.export over the http api
## [0.48.1](https://github.com/ipfs/js-ipfs/compare/ipfs@0.48.0...ipfs@0.48.1) (2020-07-21)
### Bug Fixes
* update bitswap to fix [#3182](https://github.com/ipfs/js-ipfs/issues/3182) and crypto for go-ipfs interop ([9fdbde8](https://github.com/ipfs/js-ipfs/commit/9fdbde80e976063ab56410a4d8af1ba955e32307))
## [0.48.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.47.0...ipfs@0.48.0) (2020-07-16)
### Bug Fixes
* do not list raw nodes in a dag as directories ([#3155](https://github.com/ipfs/js-ipfs/issues/3155)) ([585a142](https://github.com/ipfs/js-ipfs/commit/585a142d3c2317e80f37d6195ce24ed3146112e5))
* error when no command specified ([#3145](https://github.com/ipfs/js-ipfs/issues/3145)) ([4309e10](https://github.com/ipfs/js-ipfs/commit/4309e1004bb77ee276b57228c35a921fb780a227))
* optional arguments go in the options object ([#3118](https://github.com/ipfs/js-ipfs/issues/3118)) ([8cb8c73](https://github.com/ipfs/js-ipfs/commit/8cb8c73037e44894d756b70f344b3282463206f9))
* peer ids are strings now ([#3162](https://github.com/ipfs/js-ipfs/issues/3162)) ([281bfe6](https://github.com/ipfs/js-ipfs/commit/281bfe60f079011d0ada783a82d1f030d08a89f2))
* still load dag-pb, dag-cbor and raw when specifying custom formats ([#3132](https://github.com/ipfs/js-ipfs/issues/3132)) ([a96e3bc](https://github.com/ipfs/js-ipfs/commit/a96e3bc9e3763004beafc24b98efa85ffa665622)), closes [#3129](https://github.com/ipfs/js-ipfs/issues/3129)
* unhandledpromiserejection in electron tests ([#3146](https://github.com/ipfs/js-ipfs/issues/3146)) ([4c0c67f](https://github.com/ipfs/js-ipfs/commit/4c0c67f023c75bbcb56b0520b31f1334480a5130))
* use post for preloading ([#3149](https://github.com/ipfs/js-ipfs/issues/3149)) ([c9700f7](https://github.com/ipfs/js-ipfs/commit/c9700f78cefc523f6140361a90099c4991b427a7))
### Features
* add interface and http client versions to version output ([#3125](https://github.com/ipfs/js-ipfs/issues/3125)) ([65f8b23](https://github.com/ipfs/js-ipfs/commit/65f8b23f550f939e94aaf6939894a513519e6d68)), closes [#2878](https://github.com/ipfs/js-ipfs/issues/2878)
* add size-only flag to cli repo stat command ([#3143](https://github.com/ipfs/js-ipfs/issues/3143)) ([b4d3bf8](https://github.com/ipfs/js-ipfs/commit/b4d3bf80e7cd5820e2561fc957a9f0f17235df05))
* enable DHT by Routing.Type config key ([#3153](https://github.com/ipfs/js-ipfs/issues/3153)) ([dfe15d7](https://github.com/ipfs/js-ipfs/commit/dfe15d7422579afce8860f6321575454826d1844))
* store blocks by multihash instead of CID ([#3124](https://github.com/ipfs/js-ipfs/issues/3124)) ([03b17f5](https://github.com/ipfs/js-ipfs/commit/03b17f5e2d290e84aa0cb541079b79e468e7d1bd))
* turn on delegate nodes by default ([#3148](https://github.com/ipfs/js-ipfs/issues/3148)) ([3fd2ca8](https://github.com/ipfs/js-ipfs/commit/3fd2ca8c7bb3a907cc74d48516481fae01d47327))
## [0.47.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.46.0...ipfs@0.47.0) (2020-06-24)
### Bug Fixes
* libp2p now requires encryption module ([#3085](https://github.com/ipfs/js-ipfs/issues/3085)) ([c567282](https://github.com/ipfs/js-ipfs/commit/c56728209f0eea63d00c68163c74cfdd350de69c))
### Features
* add config.getAll ([#3071](https://github.com/ipfs/js-ipfs/issues/3071)) ([16587f1](https://github.com/ipfs/js-ipfs/commit/16587f16e1b3ae525c099b1975748510638aceee))
* libp2p noise as fallback for secio ([#3074](https://github.com/ipfs/js-ipfs/issues/3074)) ([660d3db](https://github.com/ipfs/js-ipfs/commit/660d3db9a47bff652057762b52a25529ab37117f))
* persist peerstore ([#3072](https://github.com/ipfs/js-ipfs/issues/3072)) ([b404974](https://github.com/ipfs/js-ipfs/commit/b40497427b7d33f52803c8fa14cc73be7f872d65))
* webui v2.9.0 ([#3054](https://github.com/ipfs/js-ipfs/issues/3054)) ([5d9d331](https://github.com/ipfs/js-ipfs/commit/5d9d331ed42f3ac9efc243878011db871b742a4e))
## [0.46.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.45.0...ipfs@0.46.0) (2020-06-05)
### Bug Fixes
* handle optional key to config.get ([#3069](https://github.com/ipfs/js-ipfs/issues/3069)) ([d043138](https://github.com/ipfs/js-ipfs/commit/d043138be2c0c7fd458131d56e235edec1504ca3))
### Features
* sync with go-ipfs 0.5 ([#3013](https://github.com/ipfs/js-ipfs/issues/3013)) ([0900bb9](https://github.com/ipfs/js-ipfs/commit/0900bb9b8123edb689a137a006c5507d8503f693))
## [0.45.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.44.0...ipfs@0.45.0) (2020-05-29)
### Features
* upgrade bitswap to use 1.2.0 and better wantlist performance ([18283dd](https://github.com/ipfs/js-ipfs/commit/18283dd8fb70af5ed93236482b2a5f89515c24e0))
## [0.44.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.43.3...ipfs@0.44.0) (2020-05-18)
### Bug Fixes
* fixes browser script tag example ([#3034](https://github.com/ipfs/js-ipfs/issues/3034)) ([ee8b769](https://github.com/ipfs/js-ipfs/commit/ee8b769b96f7e3c8414bbf85853ab4e21e8fd11c)), closes [#3027](https://github.com/ipfs/js-ipfs/issues/3027)
* remove ipld all formats and fix traverse ipld example ([#3025](https://github.com/ipfs/js-ipfs/issues/3025)) ([e6079c1](https://github.com/ipfs/js-ipfs/commit/e6079c17d5656e92dd5191f0581000c6a782c7ed))
* remove node globals ([#2932](https://github.com/ipfs/js-ipfs/issues/2932)) ([d0d2f74](https://github.com/ipfs/js-ipfs/commit/d0d2f74cef4e439c6d2baadba1f1f9f52534fcba))
### Features
* cancellable api calls ([#2993](https://github.com/ipfs/js-ipfs/issues/2993)) ([2b24f59](https://github.com/ipfs/js-ipfs/commit/2b24f590041a0df9da87b75ae2344232fe22fe3a)), closes [#3015](https://github.com/ipfs/js-ipfs/issues/3015)
## [0.43.3](https://github.com/ipfs/js-ipfs/compare/ipfs@0.43.2...ipfs@0.43.3) (2020-05-05)
**Note:** Version bump only for package ipfs
## [0.43.2](https://github.com/ipfs/js-ipfs/compare/ipfs@0.43.1...ipfs@0.43.2) (2020-05-05)
### Bug Fixes
* pass headers to request ([#3018](https://github.com/ipfs/js-ipfs/issues/3018)) ([3ba00f8](https://github.com/ipfs/js-ipfs/commit/3ba00f8c6a8a057c5776d539a671a74d9565fb29)), closes [#3017](https://github.com/ipfs/js-ipfs/issues/3017)
## [0.43.1](https://github.com/ipfs/js-ipfs/compare/ipfs@0.43.0...ipfs@0.43.1) (2020-04-28)
### Bug Fixes
* correct dht reference in ipns routing ([#2996](https://github.com/ipfs/js-ipfs/issues/2996)) ([d2579c0](https://github.com/ipfs/js-ipfs/commit/d2579c0e8f1e81c1a2df578d46459c7a1eeeba53))
### Features
* webui v2.7.5, with feeling ([#2984](https://github.com/ipfs/js-ipfs/issues/2984)) ([2e0a114](https://github.com/ipfs/js-ipfs/commit/2e0a1144d9405f1a34fcd038361ad075968d841f))
## [0.43.0](https://github.com/ipfs/js-ipfs/compare/ipfs@0.42.1...ipfs@0.43.0) (2020-04-16)
### Bug Fixes
* make http api only accept POST requests ([#2977](https://github.com/ipfs/js-ipfs/issues/2977)) ([943d4a8](https://github.com/ipfs/js-ipfs/commit/943d4a8cf2d4c4ff5ecd4814c59cb0aae0cfa1fd))
* regression that dht could not be enabled through conf ([#2976](https://github.com/ipfs/js-ipfs/issues/2976)) ([9d88a2e](https://github.com/ipfs/js-ipfs/commit/9d88a2ebbad4dfa58df351d31d201eaf2aaf78dc))
### BREAKING CHANGES
* Where we used to accept all and any HTTP methods, now only POST is
accepted. The API client will now only send POST requests too.
* test: add tests to make sure we are post-only
* chore: upgrade ipfs-utils
* fix: return 405 instead of 404 for bad methods
* fix: reject browsers that do not send an origin
Also fixes running interface tests over http in browsers against
js-ipfs
## [0.42.1](https://github.com/ipfs/js-ipfs/compare/ipfs@0.42.0...ipfs@0.42.1) (2020-04-08)
### Bug Fixes
* use correct name for webrtc transport config ([#2966](https://github.com/ipfs/js-ipfs/issues/2966)) ([83ca42a](https://github.com/ipfs/js-ipfs/commit/83ca42a83fbe43a93d3d66d7c117123c9423359b)), closes [#2963](https://github.com/ipfs/js-ipfs/issues/2963)
# 0.42.0 (2020-03-31)
### Bug Fixes
* add default args for ipfs.add ([#2950](https://github.com/ipfs/js-ipfs/issues/2950)) ([a01f5b6](https://github.com/ipfs/js-ipfs/commit/a01f5b63cd92d225b10eff497f79caf4baab1973))
* add default for cid base and fix cid version override ([d951993](https://github.com/ipfs/js-ipfs/commit/d9519931642fbeabd4a04940e67911e346106814))
* dont include util.textencoder in the browser ([#2919](https://github.com/ipfs/js-ipfs/issues/2919)) ([3207e3b](https://github.com/ipfs/js-ipfs/commit/3207e3b35c9c250332c03dd2a066e8ebcda35e43))
* error when command is unknown ([#2916](https://github.com/ipfs/js-ipfs/issues/2916)) ([743a7fc](https://github.com/ipfs/js-ipfs/commit/743a7fc1630e753568e8a56a8f3580cb2b8d50ad))
* multiaddr validation to add peer id for listening ([#2833](https://github.com/ipfs/js-ipfs/issues/2833)) ([78cbec1](https://github.com/ipfs/js-ipfs/commit/78cbec159ed84b1bc4cd86eb17d3c2d050827a6d))
* only start prometheus metrics in one place ([#2954](https://github.com/ipfs/js-ipfs/issues/2954)) ([d52a41e](https://github.com/ipfs/js-ipfs/commit/d52a41e1db601db55cf8433c9a91c2ee6b9b3e09)), closes [#2019](https://github.com/ipfs/js-ipfs/issues/2019)
* reuse columns value from process.stdout ([e8646d8](https://github.com/ipfs/js-ipfs/commit/e8646d874bbbdf51aa1c8df83f8d5e52a45592be))
* tag stdin with mtime and mode when piping to cli 'add' ([#2832](https://github.com/ipfs/js-ipfs/issues/2832)) ([8c97de1](https://github.com/ipfs/js-ipfs/commit/8c97de1e85c8544976a8240bf72e850f0e49a2b0)), closes [#2763](https://github.com/ipfs/js-ipfs/issues/2763)
### chore
* move mfs and multipart files into core ([#2811](https://github.com/ipfs/js-ipfs/issues/2811)) ([82b9e08](https://github.com/ipfs/js-ipfs/commit/82b9e085330e6c6290e6f3dd29678247984ffdce))
* update dep version and ignore interop test for raw leaves ([#2747](https://github.com/ipfs/js-ipfs/issues/2747)) ([6376cec](https://github.com/ipfs/js-ipfs/commit/6376cec2b4beccef4751c498088f600ec7788118))
### Features
* remove ky from http-client and utils ([#2810](https://github.com/ipfs/js-ipfs/issues/2810)) ([9bc9625](https://github.com/ipfs/js-ipfs/commit/9bc96252686d0bbbfdb2a3300bb17b80eafdaf00)), closes [#2801](https://github.com/ipfs/js-ipfs/issues/2801)
* support mtime-nsecs in mfs cli ([#2958](https://github.com/ipfs/js-ipfs/issues/2958)) ([69c091d](https://github.com/ipfs/js-ipfs/commit/69c091da963d974e75638a63c36140c8e9d3c4e0)), closes [#2803](https://github.com/ipfs/js-ipfs/issues/2803)
### BREAKING CHANGES
* When the path passed to `ipfs.files.stat(path)` was a hamt sharded dir, the resovled
value returned by js-ipfs previously had a `type` property of with a value of
`'hamt-sharded-directory'`. To bring it in line with go-ipfs this value is now
`'directory'`.
* Files that fit into one block imported with either `--cid-version=1`
or `--raw-leaves=true` previously returned a CID that resolved to
a raw node (e.g. a buffer). Returned CIDs now resolve to a `dag-pb`
node that contains a UnixFS entry. This is to allow setting metadata
on small files with CIDv1.
## [0.41.0](https://github.com/ipfs/js-ipfs/compare/v0.41.0-rc.2...v0.41.0) (2020-02-13)
### Bug Fixes
* await on things that need awaiting on ([#2773](https://github.com/ipfs/js-ipfs/issues/2773)) ([b94fe54](https://github.com/ipfs/js-ipfs/commit/b94fe54))
## [0.41.0-rc.2](https://github.com/ipfs/js-ipfs/compare/v0.41.0-rc.1...v0.41.0-rc.2) (2020-02-11)
## [0.41.0-rc.1](https://github.com/ipfs/js-ipfs/compare/v0.41.0-rc.0...v0.41.0-rc.1) (2020-02-10)
### Bug Fixes
* block put with CID as string ([#2760](https://github.com/ipfs/js-ipfs/issues/2760)) ([cc9a933](https://github.com/ipfs/js-ipfs/commit/cc9a933))
* report correct swarm addresses after listening on new addrs ([#2749](https://github.com/ipfs/js-ipfs/issues/2749)) ([41a7e55](https://github.com/ipfs/js-ipfs/commit/41a7e55)), closes [#2508](https://github.com/ipfs/js-ipfs/issues/2508)
### Features
* use it-tar ([#2758](https://github.com/ipfs/js-ipfs/issues/2758)) ([eb33a63](https://github.com/ipfs/js-ipfs/commit/eb33a63))
## [0.41.0-rc.0](https://github.com/ipfs/js-ipfs/compare/v0.40.0...v0.41.0-rc.0) (2020-02-03)
### Bug Fixes
* bad merge ([714e540](https://github.com/ipfs/js-ipfs/commit/714e540))
* correct redirect when it loads webui ([#2697](https://github.com/ipfs/js-ipfs/issues/2697)) ([#2698](https://github.com/ipfs/js-ipfs/issues/2698)) ([3516bb8](https://github.com/ipfs/js-ipfs/commit/3516bb8))
* ipfs-repo version ([f08758e](https://github.com/ipfs/js-ipfs/commit/f08758e))
* limit SW registration to content root ([#2682](https://github.com/ipfs/js-ipfs/issues/2682)) ([feba661](https://github.com/ipfs/js-ipfs/commit/feba661)), closes [/github.com/ipfs/go-ipfs/issues/4025#issuecomment-342250616](https://github.com//github.com/ipfs/go-ipfs/issues/4025/issues/issuecomment-342250616)
* repo gc error key name ([#2618](https://github.com/ipfs/js-ipfs/issues/2618)) ([5a1d266](https://github.com/ipfs/js-ipfs/commit/5a1d266)), closes [/docs.ipfs.io/reference/api/http/#api-v0](https://github.com//docs.ipfs.io/reference/api/http//issues/api-v0)
* support legacy links in cbor data ([#2631](https://github.com/ipfs/js-ipfs/issues/2631)) ([f98023b](https://github.com/ipfs/js-ipfs/commit/f98023b))
### Code Refactoring
* return peer IDs as strings not CIDs ([#2729](https://github.com/ipfs/js-ipfs/issues/2729)) ([16d540c](https://github.com/ipfs/js-ipfs/commit/16d540c))
### Features
* add --hidden flag to cli add command ([#2649](https://github.com/ipfs/js-ipfs/issues/2649)) ([ed886f4](https://github.com/ipfs/js-ipfs/commit/ed886f4))
* add alias for `ipfs repo stat --human`. ([#2609](https://github.com/ipfs/js-ipfs/issues/2609)) ([f81086c](https://github.com/ipfs/js-ipfs/commit/f81086c))
* add bitswap stat human option in CLI ([#2619](https://github.com/ipfs/js-ipfs/issues/2619)) ([6a2ea52](https://github.com/ipfs/js-ipfs/commit/6a2ea52))
* add human flag to repo stat cli command ([#2630](https://github.com/ipfs/js-ipfs/issues/2630)) ([39bc5b4](https://github.com/ipfs/js-ipfs/commit/39bc5b4))
* pass libp2pOptions to the bundle function ([#2591](https://github.com/ipfs/js-ipfs/issues/2591)) ([e8e9b91](https://github.com/ipfs/js-ipfs/commit/e8e9b91))
* return CID of flushed path from ipfs.files.flush ([#2715](https://github.com/ipfs/js-ipfs/issues/2715)) ([5db7c29](https://github.com/ipfs/js-ipfs/commit/5db7c29))
* support -X special permissions arg to files.chmod ([#2719](https://github.com/ipfs/js-ipfs/issues/2719)) ([d6ece05](https://github.com/ipfs/js-ipfs/commit/d6ece05))
* support UnixFSv1.5 metadata ([#2621](https://github.com/ipfs/js-ipfs/issues/2621)) ([acbda68](https://github.com/ipfs/js-ipfs/commit/acbda68)), closes [ipfs/js-datastore-pubsub#20](https://github.com/ipfs/js-datastore-pubsub/issues/20)
* web ui 2.7.1 ([#2599](https://github.com/ipfs/js-ipfs/issues/2599)) ([06340ec](https://github.com/ipfs/js-ipfs/commit/06340ec))
* web ui 2.7.2 ([#2651](https://github.com/ipfs/js-ipfs/issues/2651)) ([7a87d8f](https://github.com/ipfs/js-ipfs/commit/7a87d8f))
### Performance Improvements
* expose importer concurrency controls when adding files ([#2637](https://github.com/ipfs/js-ipfs/issues/2637)) ([1d19c4f](https://github.com/ipfs/js-ipfs/commit/1d19c4f))
### BREAKING CHANGES
* Where `PeerID`s were previously [CID](https://www.npmjs.com/package/cids)s, now they are Strings
- `ipfs.bitswap.stat().peers[n]` is now a String (was a CID)
- `ipfs.dht.findPeer().id` is now a String (was a CID)
- `ipfs.dht.findProvs()[n].id` is now a String (was a CID)
- `ipfs.dht.provide()[n].id` is now a String (was a CID)
- `ipfs.dht.put()[n].id` is now a String (was a CID)
- `ipfs.dht.query()[n].id` is now a String (was a CID)
- `ipfs.id().id` is now a String (was a CID)
- `ipfs.id().addresses[n]` are now [Multiaddr](https://www.npmjs.com/package/multiaddr)s (were Strings)
* Removes all `codec`/`format` options, everything is `dag-pb` now.
## [0.40.0](https://github.com/ipfs/js-ipfs/compare/v0.40.0-rc.1...v0.40.0) (2019-12-02)
## [0.40.0-rc.1](https://github.com/ipfs/js-ipfs/compare/v0.40.0-rc.0...v0.40.0-rc.1) (2019-11-28)
### Bug Fixes
* support legacy links in cbor data ([#2631](https://github.com/ipfs/js-ipfs/issues/2631)) ([3f446d6](https://github.com/ipfs/js-ipfs/commit/3f446d6))
## [0.40.0-rc.0](https://github.com/ipfs/js-ipfs/compare/v0.39.0...v0.40.0-rc.0) (2019-11-11)
### Bug Fixes
* add profiles docs, support in validation and tests ([#2545](https://github.com/ipfs/js-ipfs/issues/2545)) ([37073e6](https://github.com/ipfs/js-ipfs/commit/37073e6))
* choose import strategy in ipfs.add ([#2541](https://github.com/ipfs/js-ipfs/issues/2541)) ([bba1537](https://github.com/ipfs/js-ipfs/commit/bba1537))
* dht.provide() should accept string keys ([#2573](https://github.com/ipfs/js-ipfs/issues/2573)) ([#2589](https://github.com/ipfs/js-ipfs/issues/2589)) ([53c2144](https://github.com/ipfs/js-ipfs/commit/53c2144))
* fix ls crash ([#2546](https://github.com/ipfs/js-ipfs/issues/2546)) ([09041c3](https://github.com/ipfs/js-ipfs/commit/09041c3)), closes [ipfs/js-ipfs-unixfs-exporter#24](https://github.com/ipfs/js-ipfs-unixfs-exporter/issues/24)
* make init options look like go-ipfs ([#2544](https://github.com/ipfs/js-ipfs/issues/2544)) ([13a8289](https://github.com/ipfs/js-ipfs/commit/13a8289))
* remove superfluous backtick ([3bd47c3](https://github.com/ipfs/js-ipfs/commit/3bd47c3))
* revert evergreen webui ([#2557](https://github.com/ipfs/js-ipfs/issues/2557)) ([16806d9](https://github.com/ipfs/js-ipfs/commit/16806d9))
* **package:** update [@hapi](https://github.com/hapi)/ammo to version 4.0.0 ([#2538](https://github.com/ipfs/js-ipfs/issues/2538)) ([da78142](https://github.com/ipfs/js-ipfs/commit/da78142))
### Features
* add mssing `dag put` and `dag resolve` cli commands ([#2521](https://github.com/ipfs/js-ipfs/issues/2521)) ([8759bf8](https://github.com/ipfs/js-ipfs/commit/8759bf8))
* evergreen web ui ([#2520](https://github.com/ipfs/js-ipfs/issues/2520)) ([069bf73](https://github.com/ipfs/js-ipfs/commit/069bf73))
* integrate ipfs-repo-migrations tool ([#2527](https://github.com/ipfs/js-ipfs/issues/2527)) ([1d12ffb](https://github.com/ipfs/js-ipfs/commit/1d12ffb))
* support CIDs in /ipns/ content paths ([#2566](https://github.com/ipfs/js-ipfs/issues/2566)) ([4fa39fb](https://github.com/ipfs/js-ipfs/commit/4fa39fb))
* web ui 2.6.0 ([#2576](https://github.com/ipfs/js-ipfs/issues/2576)) ([a61d510](https://github.com/ipfs/js-ipfs/commit/a61d510))
## [0.39.0](https://github.com/ipfs/js-ipfs/compare/v0.39.0-rc.2...v0.39.0) (2019-10-23)
## [0.39.0-rc.2](https://github.com/ipfs/js-ipfs/compare/v0.39.0-rc.1...v0.39.0-rc.2) (2019-10-17)
### Bug Fixes
* add profiles docs, support in validation and tests ([#2545](https://github.com/ipfs/js-ipfs/issues/2545)) ([e081e16](https://github.com/ipfs/js-ipfs/commit/e081e16))
* choose import strategy in ipfs.add ([#2541](https://github.com/ipfs/js-ipfs/issues/2541)) ([e2e6701](https://github.com/ipfs/js-ipfs/commit/e2e6701))
* fix ls crash ([#2546](https://github.com/ipfs/js-ipfs/issues/2546)) ([83eb99b](https://github.com/ipfs/js-ipfs/commit/83eb99b)), closes [ipfs/js-ipfs-unixfs-exporter#24](https://github.com/ipfs/js-ipfs-unixfs-exporter/issues/24)
* make init options look like go-ipfs ([#2544](https://github.com/ipfs/js-ipfs/issues/2544)) ([d4d6dfe](https://github.com/ipfs/js-ipfs/commit/d4d6dfe))
## [0.39.0-rc.1](https://github.com/ipfs/js-ipfs/compare/v0.39.0-rc.0...v0.39.0-rc.1) (2019-10-15)
## [0.39.0-rc.0](https://github.com/ipfs/js-ipfs/compare/v0.38.0...v0.39.0-rc.0) (2019-10-08)
### Bug Fixes
* limit concurrent HTTP requests in browser ([#2304](https://github.com/ipfs/js-ipfs/issues/2304)) ([cf38aea](https://github.com/ipfs/js-ipfs/commit/cf38aea))
* only try to get ipfs if argv is present ([#2504](https://github.com/ipfs/js-ipfs/issues/2504)) ([1281b9f](https://github.com/ipfs/js-ipfs/commit/1281b9f))
* pull in preconfigured chai from interface tests ([#2510](https://github.com/ipfs/js-ipfs/issues/2510)) ([8c01259](https://github.com/ipfs/js-ipfs/commit/8c01259))
### Features
* Add config profile endpoint and CLI ([#2165](https://github.com/ipfs/js-ipfs/issues/2165)) ([7314f0d](https://github.com/ipfs/js-ipfs/commit/7314f0d))
* allow daemon to init and start in a single cmd ([#2428](https://github.com/ipfs/js-ipfs/issues/2428)) ([16d5e7b](https://github.com/ipfs/js-ipfs/commit/16d5e7b))
* support block.rm over http api ([#2514](https://github.com/ipfs/js-ipfs/issues/2514)) ([c9be79e](https://github.com/ipfs/js-ipfs/commit/c9be79e))
* web ui 2.5.3 ([4f398fc](https://github.com/ipfs/js-ipfs/commit/4f398fc))
* web ui 2.5.4 ([#2478](https://github.com/ipfs/js-ipfs/issues/2478)) ([bff402c](https://github.com/ipfs/js-ipfs/commit/bff402c))
### Reverts
* e ([55c4446](https://github.com/ipfs/js-ipfs/commit/55c4446))
## [0.38.0](https://github.com/ipfs/js-ipfs/compare/v0.38.0-rc.6...v0.38.0) (2019-09-30)
## [0.38.0-rc.6](https://github.com/ipfs/js-ipfs/compare/v0.38.0-rc.5...v0.38.0-rc.6) (2019-09-25)
## [0.38.0-rc.5](https://github.com/ipfs/js-ipfs/compare/v0.38.0-rc.4...v0.38.0-rc.5) (2019-09-18)
## [0.38.0-rc.4](https://github.com/ipfs/js-ipfs/compare/v0.38.0-rc.3...v0.38.0-rc.4) (2019-09-17)
## [0.38.0-rc.3](https://github.com/ipfs/js-ipfs/compare/v0.38.0-rc.2...v0.38.0-rc.3) (2019-09-17)
## [0.38.0-rc.2](https://github.com/ipfs/js-ipfs/compare/v0.38.0-rc.1...v0.38.0-rc.2) (2019-09-16)
## [0.38.0-rc.1](https://github.com/ipfs/js-ipfs/compare/v0.38.0-rc.0...v0.38.0-rc.1) (2019-09-13)
## [0.38.0-rc.0](https://github.com/ipfs/js-ipfs/compare/v0.38.0-pre.1...v0.38.0-rc.0) (2019-09-09)
### Bug Fixes
* **tests:** remove Math.random from tests ([#2431](https://github.com/ipfs/js-ipfs/issues/2431)) ([60bf020](https://github.com/ipfs/js-ipfs/commit/60bf020))
### Features
* add support for ipns and recursive to ipfs resolve ([#2297](https://github.com/ipfs/js-ipfs/issues/2297)) ([039675e](https://github.com/ipfs/js-ipfs/commit/039675e))
* enable pubsub via config file and enabled by default ([#2427](https://github.com/ipfs/js-ipfs/issues/2427)) ([27751cf](https://github.com/ipfs/js-ipfs/commit/27751cf)), closes [ipfs/js-ipfsd-ctl#366](https://github.com/ipfs/js-ipfsd-ctl/issues/366) [ipfs/go-ipfs#6621](https://github.com/ipfs/go-ipfs/issues/6621)
* support adding async iterators ([#2379](https://github.com/ipfs/js-ipfs/issues/2379)) ([3878f0f](https://github.com/ipfs/js-ipfs/commit/3878f0f))
* web ui 2.5.1 ([#2434](https://github.com/ipfs/js-ipfs/issues/2434)) ([39ef553](https://github.com/ipfs/js-ipfs/commit/39ef553))
### BREAKING CHANGES
* pubsub is now enabled by default and the experimental flag was removed
* `recursive` is now `true` by default in `ipfs resolve`
## [0.38.0-pre.1](https://github.com/ipfs/js-ipfs/compare/v0.38.0-pre.0...v0.38.0-pre.1) (2019-09-02)
### Bug Fixes
* **package:** update ipfs-http-client to version 34.0.0 ([#2407](https://github.com/ipfs/js-ipfs/issues/2407)) ([f7e5094](https://github.com/ipfs/js-ipfs/commit/f7e5094))
### Features
* gossipsub as default pubsub ([#2298](https://github.com/ipfs/js-ipfs/issues/2298)) ([902e045](https://github.com/ipfs/js-ipfs/commit/902e045))
### Reverts
* update of ipfs-http-client ([#2412](https://github.com/ipfs/js-ipfs/issues/2412)) ([f6cf876](https://github.com/ipfs/js-ipfs/commit/f6cf876))
### BREAKING CHANGES
* The default pubsub implementation has changed from floodsub to [gossipsub](https://github.com/ChainSafe/gossipsub-js). Additionally, to enable pubsub programmatically set `pubsub.enabled: true` instead of `EXPERIMENTAL.pubsub: true` or via the CLI pass `--enable-pubsub` instead of `--enable-pubsub-experiment` to `jsipfs daemon`.
## [0.38.0-pre.0](https://github.com/ipfs/js-ipfs/compare/v0.37.1...v0.38.0-pre.0) (2019-08-27)
### Bug Fixes
* make progress bar work again when adding files ([#2386](https://github.com/ipfs/js-ipfs/issues/2386)) ([f6dcb0f](https://github.com/ipfs/js-ipfs/commit/f6dcb0f)), closes [#2379](https://github.com/ipfs/js-ipfs/issues/2379)
* really allow logo to appear on npm ([02e521e](https://github.com/ipfs/js-ipfs/commit/02e521e))
### Features
* garbage collection ([#2022](https://github.com/ipfs/js-ipfs/issues/2022)) ([44045b0](https://github.com/ipfs/js-ipfs/commit/44045b0))
### BREAKING CHANGES
* Order of output from the CLI command `ipfs add` may change between invocations given the same files since it is not longer buffered and sorted.
Previously, js-ipfs buffered all hashes of added files and sorted them before outputting to the terminal, this might be because the `glob` module does not return stable results. This gives a poor user experience as you see nothing then everything, compared to `go-ipfs` which prints out the hashes as they become available. The tradeoff is the order might be slightly different between invocations, though the hashes are always the same as we sort DAGNode links before serializing so it doesn't matter what order you import them in.
## [0.37.1](https://github.com/ipfs/js-ipfs/compare/v0.37.0...v0.37.1) (2019-08-23)
### Bug Fixes
* create HTTP servers in series ([#2388](https://github.com/ipfs/js-ipfs/issues/2388)) ([970a269](https://github.com/ipfs/js-ipfs/commit/970a269))
* enable preload on MFS commands that accept IPFS paths ([#2355](https://github.com/ipfs/js-ipfs/issues/2355)) ([0e0d1dd](https://github.com/ipfs/js-ipfs/commit/0e0d1dd))
* **package:** update yargs to version 14.0.0 ([#2371](https://github.com/ipfs/js-ipfs/issues/2371)) ([5aadb2d](https://github.com/ipfs/js-ipfs/commit/5aadb2d))
* do not load all of a DAG into memory when pinning ([#2372](https://github.com/ipfs/js-ipfs/issues/2372)) ([f357c28](https://github.com/ipfs/js-ipfs/commit/f357c28)), closes [#2310](https://github.com/ipfs/js-ipfs/issues/2310)
* preload addreses with trailing slash ([#2377](https://github.com/ipfs/js-ipfs/issues/2377)) ([c607971](https://github.com/ipfs/js-ipfs/commit/c607971)), closes [#2333](https://github.com/ipfs/js-ipfs/issues/2333)
### Features
* allow controlling preload from cli and http api ([#2384](https://github.com/ipfs/js-ipfs/issues/2384)) ([5878a0a](https://github.com/ipfs/js-ipfs/commit/5878a0a))
* resolution of .eth names via .eth.link ([#2373](https://github.com/ipfs/js-ipfs/issues/2373)) ([7e02140](https://github.com/ipfs/js-ipfs/commit/7e02140))
## [0.37.0](https://github.com/ipfs/js-ipfs/compare/v0.37.0-rc.1...v0.37.0) (2019-08-06)
## [0.37.0-rc.1](https://github.com/ipfs/js-ipfs/compare/v0.37.0-rc.0...v0.37.0-rc.1) (2019-08-06)
### Bug Fixes
* add CORS headers to gateway responses ([#2254](https://github.com/ipfs/js-ipfs/issues/2254)) ([5156a47](https://github.com/ipfs/js-ipfs/commit/5156a47))
* disable socket timeout for pubsub subscriptions ([#2303](https://github.com/ipfs/js-ipfs/issues/2303)) ([3583cc2](https://github.com/ipfs/js-ipfs/commit/3583cc2))
* move mfs cmds and safer exit ([#1981](https://github.com/ipfs/js-ipfs/issues/1981)) ([fee0141](https://github.com/ipfs/js-ipfs/commit/fee0141))
* swarm.peers latency value when unknown ([#2336](https://github.com/ipfs/js-ipfs/issues/2336)) ([6248ec1](https://github.com/ipfs/js-ipfs/commit/6248ec1))
* use ephemeral ports in API/Gateway bind test ([#2305](https://github.com/ipfs/js-ipfs/issues/2305)) ([24679af](https://github.com/ipfs/js-ipfs/commit/24679af))
* **package:** update ipfs-mfs to version 0.12.0 ([#2275](https://github.com/ipfs/js-ipfs/issues/2275)) ([c15f146](https://github.com/ipfs/js-ipfs/commit/c15f146))
## [0.37.0-rc.0](https://github.com/ipfs/js-ipfs/compare/v0.36.4...v0.37.0-rc.0) (2019-07-17)
### Bug Fixes
* allow setting Addresses.Delegates ([#2253](https://github.com/ipfs/js-ipfs/issues/2253)) ([58a9bc4](https://github.com/ipfs/js-ipfs/commit/58a9bc4))
* browser video streaming example ([#2267](https://github.com/ipfs/js-ipfs/issues/2267)) ([f5cf216](https://github.com/ipfs/js-ipfs/commit/f5cf216))
* clean repo func on windows ([#2243](https://github.com/ipfs/js-ipfs/issues/2243)) ([26b92a1](https://github.com/ipfs/js-ipfs/commit/26b92a1))
* failing test on config ([#2205](https://github.com/ipfs/js-ipfs/issues/2205)) ([5ed9532](https://github.com/ipfs/js-ipfs/commit/5ed9532))
* ipns reference to libp2p dht config ([#2182](https://github.com/ipfs/js-ipfs/issues/2182)) ([e46e6ad](https://github.com/ipfs/js-ipfs/commit/e46e6ad))
* passed config validation ([#2270](https://github.com/ipfs/js-ipfs/issues/2270)) ([80e7d81](https://github.com/ipfs/js-ipfs/commit/80e7d81))
* pin type filtering in pin.ls ([#2228](https://github.com/ipfs/js-ipfs/issues/2228)) ([afdfe7f](https://github.com/ipfs/js-ipfs/commit/afdfe7f))
* **gateway:** disable compression ([#2245](https://github.com/ipfs/js-ipfs/issues/2245)) ([4ee28e0](https://github.com/ipfs/js-ipfs/commit/4ee28e0))
* **package:** update file-type to version 12.0.0 ([#2176](https://github.com/ipfs/js-ipfs/issues/2176)) ([3e63ef2](https://github.com/ipfs/js-ipfs/commit/3e63ef2))
### Code Refactoring
* **gateway:** return implicit index.html ([#2217](https://github.com/ipfs/js-ipfs/issues/2217)) ([8519886](https://github.com/ipfs/js-ipfs/commit/8519886))
### Features
* add delegate routers to libp2p config ([#2195](https://github.com/ipfs/js-ipfs/issues/2195)) ([1aaaab9](https://github.com/ipfs/js-ipfs/commit/1aaaab9))
* add HTTP Gateway support for /ipns/ paths ([#2020](https://github.com/ipfs/js-ipfs/issues/2020)) ([43ac305](https://github.com/ipfs/js-ipfs/commit/43ac305)), closes [#1989](https://github.com/ipfs/js-ipfs/issues/1989)
* add support for ipns name resolve /ipns/ ([#2002](https://github.com/ipfs/js-ipfs/issues/2002)) ([5044a30](https://github.com/ipfs/js-ipfs/commit/5044a30)), closes [#1918](https://github.com/ipfs/js-ipfs/issues/1918)
* randomly pick preload node ([#2194](https://github.com/ipfs/js-ipfs/issues/2194)) ([f596b01](https://github.com/ipfs/js-ipfs/commit/f596b01))
* ready promise ([#2094](https://github.com/ipfs/js-ipfs/issues/2094)) ([e0994f2](https://github.com/ipfs/js-ipfs/commit/e0994f2))
* update Web UI to v2.4.6 ([#2147](https://github.com/ipfs/js-ipfs/issues/2147)) ([a1a9fe3](https://github.com/ipfs/js-ipfs/commit/a1a9fe3))
### BREAKING CHANGES
* **gateway:** Gateway now implicitly responds with the contents of `/index.html` when accessing a directory `/` instead of redirecting to `/index.html`.
This changes current logic (redirect to index.html) to match what
go-ipfs does (return index.html without changing URL)
We also ensure directory URLs always end with '/'
License: MIT
Signed-off-by: Marcin Rataj
## [0.36.4](https://github.com/ipfs/js-ipfs/compare/v0.36.3...v0.36.4) (2019-06-18)
## [0.36.3](https://github.com/ipfs/js-ipfs/compare/v0.36.2...v0.36.3) (2019-05-30)
### Bug Fixes
* double callback in object.links for cbor data ([#2111](https://github.com/ipfs/js-ipfs/issues/2111)) ([5d080c0](https://github.com/ipfs/js-ipfs/commit/5d080c0))
* fixes rabin chunker truncating files ([#2114](https://github.com/ipfs/js-ipfs/issues/2114)) ([76689ff](https://github.com/ipfs/js-ipfs/commit/76689ff))
* **package:** update bignumber.js to version 9.0.0 ([#2123](https://github.com/ipfs/js-ipfs/issues/2123)) ([37903ad](https://github.com/ipfs/js-ipfs/commit/37903ad))
## [0.36.2](https://github.com/ipfs/js-ipfs/compare/v0.36.1...v0.36.2) (2019-05-24)
### Bug Fixes
* file support when added as object ([#2105](https://github.com/ipfs/js-ipfs/issues/2105)) ([ba80e40](https://github.com/ipfs/js-ipfs/commit/ba80e40))
* upgrade electron examples ([#2104](https://github.com/ipfs/js-ipfs/issues/2104)) ([67e1b59](https://github.com/ipfs/js-ipfs/commit/67e1b59))
## [0.36.1](https://github.com/ipfs/js-ipfs/compare/v0.36.0...v0.36.1) (2019-05-22)
### Bug Fixes
* **cli:** make swarm addrs more resilient ([#2083](https://github.com/ipfs/js-ipfs/issues/2083)) ([3792b68](https://github.com/ipfs/js-ipfs/commit/3792b68))
## [0.36.0](https://github.com/ipfs/js-ipfs/compare/v0.36.0-rc.0...v0.36.0) (2019-05-22)
### Bug Fixes
* use trickle builder in daemon mode too ([#2085](https://github.com/ipfs/js-ipfs/issues/2085)) ([62b873f](https://github.com/ipfs/js-ipfs/commit/62b873f))
* **package:** update libp2p-kad-dht to version 0.15.0 ([#2049](https://github.com/ipfs/js-ipfs/issues/2049)) ([5905760](https://github.com/ipfs/js-ipfs/commit/5905760))
* browser-mfs example ([#2089](https://github.com/ipfs/js-ipfs/issues/2089)) ([e7d6d3a](https://github.com/ipfs/js-ipfs/commit/e7d6d3a))
* error when preloding is disabled in the browser ([#2086](https://github.com/ipfs/js-ipfs/issues/2086)) ([a56bcbf](https://github.com/ipfs/js-ipfs/commit/a56bcbf))
* traverse-ipld-graphs (tree) example ([#2088](https://github.com/ipfs/js-ipfs/issues/2088)) ([b5c652f](https://github.com/ipfs/js-ipfs/commit/b5c652f))
* update option in exchange files in browser example ([#2087](https://github.com/ipfs/js-ipfs/issues/2087)) ([63469ed](https://github.com/ipfs/js-ipfs/commit/63469ed))
## [0.36.0-rc.0](https://github.com/ipfs/js-ipfs/compare/v0.36.0-pre.0...v0.36.0-rc.0) (2019-05-21)
### Code Refactoring
* update ipld formats, async/await mfs/unixfs & base32 cids ([#2068](https://github.com/ipfs/js-ipfs/issues/2068)) ([813048f](https://github.com/ipfs/js-ipfs/commit/813048f)), closes [ipld/js-ipld-dag-pb#137](https://github.com/ipld/js-ipld-dag-pb/issues/137) [ipfs/interface-js-ipfs-core#473](https://github.com/ipfs/interface-js-ipfs-core/issues/473) [ipfs/js-ipfs-http-client#1010](https://github.com/ipfs/js-ipfs-http-client/issues/1010) [ipfs/js-ipfs-http-response#25](https://github.com/ipfs/js-ipfs-http-response/issues/25) [#1995](https://github.com/ipfs/js-ipfs/issues/1995)
### BREAKING CHANGES
* The default string encoding for version 1 CIDs has changed to `base32`.
IPLD formats have been updated to the latest versions. IPLD nodes returned by `ipfs.dag` and `ipfs.object` commands have significant breaking changes. If you are using these commands in your application you are likely to encounter the following changes to `dag-pb` nodes (the default node type that IPFS creates):
* `DAGNode` properties have been renamed as follows:
* `data` => `Data`
* `links` => `Links`
* `size` => `size` (Note: no change)
* `DAGLink` properties have been renamed as follows:
* `cid` => `Hash`
* `name` => `Name`
* `size` => `Tsize`
See CHANGELOGs for each IPLD format for it's respective changes, you can read more about the [`dag-pb` changes in the CHANGELOG](https://github.com/ipld/js-ipld-dag-pb/blob/master)
License: MIT
Signed-off-by: Alan Shaw
## [0.36.0-pre.0](https://github.com/ipfs/js-ipfs/compare/v0.35.0...v0.36.0-pre.0) (2019-05-17)
### Bug Fixes
* **package:** update ipfs-http-client to version 31.0.0 ([#2052](https://github.com/ipfs/js-ipfs/issues/2052)) ([906f8d0](https://github.com/ipfs/js-ipfs/commit/906f8d0))
* correctly validate ipld config ([#2033](https://github.com/ipfs/js-ipfs/issues/2033)) ([eebc17a](https://github.com/ipfs/js-ipfs/commit/eebc17a))
* **package:** update hapi-pino to version 6.0.0 ([#2043](https://github.com/ipfs/js-ipfs/issues/2043)) ([f4e3bd0](https://github.com/ipfs/js-ipfs/commit/f4e3bd0))
### Features
* add support for File DOM API to files-regular ([#2013](https://github.com/ipfs/js-ipfs/issues/2013)) ([0a08192](https://github.com/ipfs/js-ipfs/commit/0a08192))
* implement ipfs refs and refs local ([#2004](https://github.com/ipfs/js-ipfs/issues/2004)) ([6dc9075](https://github.com/ipfs/js-ipfs/commit/6dc9075))
* **gateway:** add streaming, conditional and range requests ([#1989](https://github.com/ipfs/js-ipfs/issues/1989)) ([48a8e75](https://github.com/ipfs/js-ipfs/commit/48a8e75))
## [0.35.0](https://github.com/ipfs/js-ipfs/compare/v0.35.0-rc.7...v0.35.0) (2019-04-12)
## [0.35.0-rc.7](https://github.com/ipfs/js-ipfs/compare/v0.35.0-rc.6...v0.35.0-rc.7) (2019-04-12)
### Bug Fixes
* flakey windows test ([#1987](https://github.com/ipfs/js-ipfs/issues/1987)) ([9708c0a](https://github.com/ipfs/js-ipfs/commit/9708c0a))
* really disable DHT ([#1991](https://github.com/ipfs/js-ipfs/issues/1991)) ([2470be8](https://github.com/ipfs/js-ipfs/commit/2470be8))
* remove non default ipld formats in the browser ([#1980](https://github.com/ipfs/js-ipfs/issues/1980)) ([4376121](https://github.com/ipfs/js-ipfs/commit/4376121))
### BREAKING CHANGES
* Browser application bundles now include only `ipld-dag-pb`, `ipld-dag-cbor` and `ipld-raw` IPLD codecs. Other codecs should be added manually, see https://github.com/ipfs/js-ipfs/blob/master/README.md#optionsipld for details.
* In Node.js `require('ipfs')`
* all IPLD formats included
* In browser application bundle `require('ipfs')` bundled with webpack/browserify/etc.
* only `ipld-dag-pb`, `ipld-dag-cbor` and `ipld-raw` included
* CDN bundle ``
* all IPLD formats included
Co-Authored-By: hugomrdias
## [0.35.0-rc.6](https://github.com/ipfs/js-ipfs/compare/v0.35.0-rc.5...v0.35.0-rc.6) (2019-04-11)
### Bug Fixes
* avoid logging http errors when its logger is not on ([#1977](https://github.com/ipfs/js-ipfs/issues/1977)) ([20beea2](https://github.com/ipfs/js-ipfs/commit/20beea2))
### Features
* recursive dnslink lookups ([#1935](https://github.com/ipfs/js-ipfs/issues/1935)) ([d5a1b89](https://github.com/ipfs/js-ipfs/commit/d5a1b89))
* use libp2p auto dial ([#1983](https://github.com/ipfs/js-ipfs/issues/1983)) ([7f1fb26](https://github.com/ipfs/js-ipfs/commit/7f1fb26))
## [0.35.0-rc.5](https://github.com/ipfs/js-ipfs/compare/v0.35.0-rc.4...v0.35.0-rc.5) (2019-04-04)
### Bug Fixes
* force browserify to load Buffer module ([#1969](https://github.com/ipfs/js-ipfs/issues/1969)) ([3654e50](https://github.com/ipfs/js-ipfs/commit/3654e50))
* stop IPNS republisher ASAP ([#1976](https://github.com/ipfs/js-ipfs/issues/1976)) ([68561c8](https://github.com/ipfs/js-ipfs/commit/68561c8))
* update link for multihashes ([#1975](https://github.com/ipfs/js-ipfs/issues/1975)) ([4a01bf6](https://github.com/ipfs/js-ipfs/commit/4a01bf6))
### Features
* expose multihashing-async along with other deps ([#1974](https://github.com/ipfs/js-ipfs/issues/1974)) ([6667966](https://github.com/ipfs/js-ipfs/commit/6667966)), closes [#1973](https://github.com/ipfs/js-ipfs/issues/1973)
## [0.35.0-rc.4](https://github.com/ipfs/js-ipfs/compare/v0.35.0-rc.3...v0.35.0-rc.4) (2019-03-28)
### Bug Fixes
* CLI parsing of --silent arg ([#1955](https://github.com/ipfs/js-ipfs/issues/1955)) ([1c07779](https://github.com/ipfs/js-ipfs/commit/1c07779)), closes [#1947](https://github.com/ipfs/js-ipfs/issues/1947)
### Code Refactoring
* swap joi-browser with superstruct ([#1961](https://github.com/ipfs/js-ipfs/issues/1961)) ([8fb5825](https://github.com/ipfs/js-ipfs/commit/8fb5825))
### Performance Improvements
* reduce bundle size ([#1959](https://github.com/ipfs/js-ipfs/issues/1959)) ([a3b6235](https://github.com/ipfs/js-ipfs/commit/a3b6235))
### BREAKING CHANGES
* Constructor config validation is now a bit more strict - it does not allow `null` values or unknown properties.
## [0.35.0-rc.3](https://github.com/ipfs/js-ipfs/compare/v0.35.0-rc.2...v0.35.0-rc.3) (2019-03-21)
### Bug Fixes
* name resolve arg parsing ([#1958](https://github.com/ipfs/js-ipfs/issues/1958)) ([924690e](https://github.com/ipfs/js-ipfs/commit/924690e))
## [0.35.0-rc.2](https://github.com/ipfs/js-ipfs/compare/v0.35.0-rc.1...v0.35.0-rc.2) (2019-03-21)
## [0.35.0-rc.1](https://github.com/ipfs/js-ipfs/compare/v0.35.0-rc.0...v0.35.0-rc.1) (2019-03-20)
### Bug Fixes
* cat deeply nested file ([#1920](https://github.com/ipfs/js-ipfs/issues/1920)) ([dcb453a](https://github.com/ipfs/js-ipfs/commit/dcb453a))
* handle subdomains for ipfs.dns ([#1933](https://github.com/ipfs/js-ipfs/issues/1933)) ([29072a5](https://github.com/ipfs/js-ipfs/commit/29072a5))
* only dial to unconnected peers ([#1914](https://github.com/ipfs/js-ipfs/issues/1914)) ([1478652](https://github.com/ipfs/js-ipfs/commit/1478652))
### Features
* add HTTP DAG API ([#1930](https://github.com/ipfs/js-ipfs/issues/1930)) ([a033e8b](https://github.com/ipfs/js-ipfs/commit/a033e8b))
* display version info when starting daemon ([#1915](https://github.com/ipfs/js-ipfs/issues/1915)) ([6b789ee](https://github.com/ipfs/js-ipfs/commit/6b789ee))
* provide access to multicodec ([#1921](https://github.com/ipfs/js-ipfs/issues/1921)) ([ceec0bc](https://github.com/ipfs/js-ipfs/commit/ceec0bc)), closes [#1913](https://github.com/ipfs/js-ipfs/issues/1913)
* **issue-1852:** support multiple API and Gateway addresses ([#1903](https://github.com/ipfs/js-ipfs/issues/1903)) ([4ad104d](https://github.com/ipfs/js-ipfs/commit/4ad104d)), closes [#1852](https://github.com/ipfs/js-ipfs/issues/1852)
### Performance Improvements
* lower connection manager limits ([#1926](https://github.com/ipfs/js-ipfs/issues/1926)) ([7926349](https://github.com/ipfs/js-ipfs/commit/7926349))
## [0.35.0-rc.0](https://github.com/ipfs/js-ipfs/compare/v0.35.0-pre.0...v0.35.0-rc.0) (2019-03-06)
### Bug Fixes
* add support for resolving to the middle of an IPLD block ([#1841](https://github.com/ipfs/js-ipfs/issues/1841)) ([fc08243](https://github.com/ipfs/js-ipfs/commit/fc08243))
* dht browser disabled ([#1879](https://github.com/ipfs/js-ipfs/issues/1879)) ([7c5a843](https://github.com/ipfs/js-ipfs/commit/7c5a843))
* ipv6 multiaddr in stdout ([#1854](https://github.com/ipfs/js-ipfs/issues/1854)) ([35fd541](https://github.com/ipfs/js-ipfs/commit/35fd541)), closes [#1853](https://github.com/ipfs/js-ipfs/issues/1853)
* make clear pins function in tests serial ([#1910](https://github.com/ipfs/js-ipfs/issues/1910)) ([503e5ac](https://github.com/ipfs/js-ipfs/commit/503e5ac)), closes [#1890](https://github.com/ipfs/js-ipfs/issues/1890)
* pin.rm test EPERM rename ([#1889](https://github.com/ipfs/js-ipfs/issues/1889)) ([c60de74](https://github.com/ipfs/js-ipfs/commit/c60de74))
* temporarily disable random walk dht discovery ([#1907](https://github.com/ipfs/js-ipfs/issues/1907)) ([3fff46a](https://github.com/ipfs/js-ipfs/commit/3fff46a))
### Code Refactoring
* export types and utilities statically ([#1908](https://github.com/ipfs/js-ipfs/issues/1908)) ([79d7fef](https://github.com/ipfs/js-ipfs/commit/79d7fef))
### Features
* add `--enable-preload` to enable/disable preloading for daemons ([#1909](https://github.com/ipfs/js-ipfs/issues/1909)) ([9470900](https://github.com/ipfs/js-ipfs/commit/9470900))
* limit connections number ([#1872](https://github.com/ipfs/js-ipfs/issues/1872)) ([bebce7f](https://github.com/ipfs/js-ipfs/commit/bebce7f))
### BREAKING CHANGES
* `ipfs.util.isIPFS` and `ipfs.util.crypto` have moved to static exports and should be accessed via `const { isIPFS, crypto } = require('ipfs')`.
The modules available under `ipfs.types.*` have also become static exports.
License: MIT
Signed-off-by: Alan Shaw
* `ipfs.resolve` now supports resolving to the middle of an IPLD block instead of erroring.
Given:
```js
b = {"c": "some value"}
a = {"b": {"/": cidOf(b) }}
```
`ipfs resolve /ipld/cidOf(a)/b/c` should return `/ipld/cidOf(b)/c`. That is, it resolves the path as much as it can.
Previously it would simply fail with an error.
License: MIT
Signed-off-by: Alan Shaw
## [0.35.0-pre.0](https://github.com/ipfs/js-ipfs/compare/v0.34.4...v0.35.0-pre.0) (2019-02-11)
### Bug Fixes
* add missing libp2p-websocket-star dep ([#1869](https://github.com/ipfs/js-ipfs/issues/1869)) ([7cba3dd](https://github.com/ipfs/js-ipfs/commit/7cba3dd))
* path to cid-tool commands ([#1866](https://github.com/ipfs/js-ipfs/issues/1866)) ([506f5be](https://github.com/ipfs/js-ipfs/commit/506f5be))
* swallowed errors ([#1860](https://github.com/ipfs/js-ipfs/issues/1860)) ([47e2b9e](https://github.com/ipfs/js-ipfs/commit/47e2b9e)), closes [#1835](https://github.com/ipfs/js-ipfs/issues/1835) [#1858](https://github.com/ipfs/js-ipfs/issues/1858)
### Chores
* rename local option to offline ([#1850](https://github.com/ipfs/js-ipfs/issues/1850)) ([bbe561b](https://github.com/ipfs/js-ipfs/commit/bbe561b))
### Features
* interoperable DHT ([#856](https://github.com/ipfs/js-ipfs/issues/856)) ([77a0957](https://github.com/ipfs/js-ipfs/commit/77a0957))
### BREAKING CHANGES
* `--local` option has been renamed to `--offline`
## [0.34.4](https://github.com/ipfs/js-ipfs/compare/v0.34.3...v0.34.4) (2019-01-24)
### Features
* support _dnslink subdomain specified dnslinks ([#1843](https://github.com/ipfs/js-ipfs/issues/1843)) ([a17253e](https://github.com/ipfs/js-ipfs/commit/a17253e))
## [0.34.3](https://github.com/ipfs/js-ipfs/compare/v0.34.2...v0.34.3) (2019-01-24)
### Bug Fixes
* add cors support for preload-mock-server and update aegir ([#1839](https://github.com/ipfs/js-ipfs/issues/1839)) ([2d45c9d](https://github.com/ipfs/js-ipfs/commit/2d45c9d))
## [0.34.2](https://github.com/ipfs/js-ipfs/compare/v0.34.1...v0.34.2) (2019-01-21)
### Bug Fixes
* race condition causing Database is not open error ([#1834](https://github.com/ipfs/js-ipfs/issues/1834)) ([6066c97](https://github.com/ipfs/js-ipfs/commit/6066c97))
### Features
* use ws-star-multi instead of ws-star ([#1793](https://github.com/ipfs/js-ipfs/issues/1793)) ([21fd4d1](https://github.com/ipfs/js-ipfs/commit/21fd4d1))
## [0.34.1](https://github.com/ipfs/js-ipfs/compare/v0.34.0...v0.34.1) (2019-01-21)
### Features
* pipe to add ([#1833](https://github.com/ipfs/js-ipfs/issues/1833)) ([ea53071](https://github.com/ipfs/js-ipfs/commit/ea53071))
## [0.34.0](https://github.com/ipfs/js-ipfs/compare/v0.34.0-rc.1...v0.34.0) (2019-01-17)
## [0.34.0-rc.1](https://github.com/ipfs/js-ipfs/compare/v0.34.0-rc.0...v0.34.0-rc.1) (2019-01-15)
### Bug Fixes
* sharness tests ([#1787](https://github.com/ipfs/js-ipfs/issues/1787)) ([48d3e2b](https://github.com/ipfs/js-ipfs/commit/48d3e2b))
### Code Refactoring
* switch to bignumber.js ([#1803](https://github.com/ipfs/js-ipfs/issues/1803)) ([6de6adf](https://github.com/ipfs/js-ipfs/commit/6de6adf))
### Features
* update to Web UI v2.3.2 ([#1807](https://github.com/ipfs/js-ipfs/issues/1807)) ([8ca6471](https://github.com/ipfs/js-ipfs/commit/8ca6471))
* update Web UI to v2.3.0 ([#1786](https://github.com/ipfs/js-ipfs/issues/1786)) ([7bcc496](https://github.com/ipfs/js-ipfs/commit/7bcc496))
### BREAKING CHANGES
* All API methods that returned [`big.js`](https://github.com/MikeMcl/big.js/) instances now return [`bignumber.js`](https://github.com/MikeMcl/bignumber.js/) instances.
License: MIT
Signed-off-by: Alan Shaw
## [0.34.0-rc.0](https://github.com/ipfs/js-ipfs/compare/v0.34.0-pre.0...v0.34.0-rc.0) (2018-12-18)
### Bug Fixes
* link to Github profile for David Dias ([3659d7e](https://github.com/ipfs/js-ipfs/commit/3659d7e))
* streaming cat over http api ([#1760](https://github.com/ipfs/js-ipfs/issues/1760)) ([3ded576](https://github.com/ipfs/js-ipfs/commit/3ded576))
### Features
* add `addFromFs` method ([#1777](https://github.com/ipfs/js-ipfs/issues/1777)) ([7315aa1](https://github.com/ipfs/js-ipfs/commit/7315aa1))
* add from url/stream ([#1773](https://github.com/ipfs/js-ipfs/issues/1773)) ([b6a7ab6](https://github.com/ipfs/js-ipfs/commit/b6a7ab6))
* add slient option ([#1712](https://github.com/ipfs/js-ipfs/issues/1712)) ([593334b](https://github.com/ipfs/js-ipfs/commit/593334b))
* cid base option ([#1552](https://github.com/ipfs/js-ipfs/issues/1552)) ([6d46e2e](https://github.com/ipfs/js-ipfs/commit/6d46e2e)), closes [/github.com/ipfs/go-ipfs/issues/5349#issuecomment-445104823](https://github.com//github.com/ipfs/go-ipfs/issues/5349/issues/issuecomment-445104823)
## [0.34.0-pre.0](https://github.com/ipfs/js-ipfs/compare/v0.33.1...v0.34.0-pre.0) (2018-12-07)
### Bug Fixes
* add dash case to pin cli ([#1719](https://github.com/ipfs/js-ipfs/issues/1719)) ([eacd580](https://github.com/ipfs/js-ipfs/commit/eacd580))
* add missing dependencies ([#1663](https://github.com/ipfs/js-ipfs/issues/1663)) ([4bcf4a7](https://github.com/ipfs/js-ipfs/commit/4bcf4a7))
* allow disabling mfs preload from config ([#1733](https://github.com/ipfs/js-ipfs/issues/1733)) ([5f66538](https://github.com/ipfs/js-ipfs/commit/5f66538))
* better error message when pubsub is not enabled ([#1729](https://github.com/ipfs/js-ipfs/issues/1729)) ([5237dd9](https://github.com/ipfs/js-ipfs/commit/5237dd9))
* examples after files API refactor ([#1740](https://github.com/ipfs/js-ipfs/issues/1740)) ([34ec036](https://github.com/ipfs/js-ipfs/commit/34ec036))
* ipns datastore key ([#1741](https://github.com/ipfs/js-ipfs/issues/1741)) ([a39770e](https://github.com/ipfs/js-ipfs/commit/a39770e))
* make circuit relay test ([#1710](https://github.com/ipfs/js-ipfs/issues/1710)) ([345ce91](https://github.com/ipfs/js-ipfs/commit/345ce91))
* remove electron-webrtc and wrtc for now ([#1718](https://github.com/ipfs/js-ipfs/issues/1718)) ([b6b50d5](https://github.com/ipfs/js-ipfs/commit/b6b50d5))
### Code Refactoring
* files API ([#1720](https://github.com/ipfs/js-ipfs/issues/1720)) ([a82a5dc](https://github.com/ipfs/js-ipfs/commit/a82a5dc))
* object APIs write methods now return CIDs ([#1730](https://github.com/ipfs/js-ipfs/issues/1730)) ([ac5fa8e](https://github.com/ipfs/js-ipfs/commit/ac5fa8e)), closes [/github.com/ipfs/interface-ipfs-core/pull/388#pullrequestreview-173866270](https://github.com//github.com/ipfs/interface-ipfs-core/pull/388/issues/pullrequestreview-173866270)
### Features
* ipns over dht ([#1725](https://github.com/ipfs/js-ipfs/issues/1725)) ([1a943f8](https://github.com/ipfs/js-ipfs/commit/1a943f8))
* ipns over pubsub ([#1559](https://github.com/ipfs/js-ipfs/issues/1559)) ([8712542](https://github.com/ipfs/js-ipfs/commit/8712542))
* Web UI updated to v2.2.0 ([#1711](https://github.com/ipfs/js-ipfs/issues/1711)) ([b2158bc](https://github.com/ipfs/js-ipfs/commit/b2158bc))
### Performance Improvements
* lazy load IPLD formats ([#1704](https://github.com/ipfs/js-ipfs/issues/1704)) ([aefb261](https://github.com/ipfs/js-ipfs/commit/aefb261))
### BREAKING CHANGES
* Object API refactor.
Object API methods that write DAG nodes now return a [CID](https://www.npmjs.com/package/cids) instead of a DAG node. Affected methods:
* `ipfs.object.new`
* `ipfs.object.patch.addLink`
* `ipfs.object.patch.appendData`
* `ipfs.object.patch.rmLink`
* `ipfs.object.patch.setData`
* `ipfs.object.put`
Example:
```js
// Before
const dagNode = await ipfs.object.new()
```
```js
// After
const cid = await ipfs.object.new() // now returns a CID
const dagNode = await ipfs.object.get(cid) // fetch the DAG node that was created
```
IMPORTANT: `DAGNode` instances, which are part of the IPLD dag-pb format have been refactored.
These instances no longer have `multihash`, `cid` or `serialized` properties.
This effects the following API methods that return these types of objects:
* `ipfs.object.get`
* `ipfs.dag.get`
See https://github.com/ipld/js-ipld-dag-pb/pull/99 for more information.
License: MIT
Signed-off-by: Alan Shaw
* Files API methods `add*`, `cat*`, `get*` have moved from `files` to the root namespace.
Specifically, the following changes have been made:
* `ipfs.files.add` => `ipfs.add`
* `ipfs.files.addPullStream` => `ipfs.addPullStream`
* `ipfs.files.addReadableStream` => `ipfs.addReadableStream`
* `ipfs.files.cat` => `ipfs.cat`
* `ipfs.files.catPullStream` => `ipfs.catPullStream`
* `ipfs.files.catReadableStream` => `ipfs.catReadableStream`
* `ipfs.files.get` => `ipfs.get`
* `ipfs.files.getPullStream` => `ipfs.getPullStream`
* `ipfs.files.getReadableStream` => `ipfs.getReadableStream`
License: MIT
Signed-off-by: Alan Shaw
## [0.33.1](https://github.com/ipfs/js-ipfs/compare/v0.33.0...v0.33.1) (2018-11-05)
### Bug Fixes
* over eager preload ([#1693](https://github.com/ipfs/js-ipfs/issues/1693)) ([f14c20d](https://github.com/ipfs/js-ipfs/commit/f14c20d))
## [0.33.0](https://github.com/ipfs/js-ipfs/compare/v0.33.0-rc.4...v0.33.0) (2018-11-01)
## [0.33.0-rc.4](https://github.com/ipfs/js-ipfs/compare/v0.33.0-rc.3...v0.33.0-rc.4) (2018-11-01)
### Bug Fixes
* remove accidentally committed code ([66fa8ef](https://github.com/ipfs/js-ipfs/commit/66fa8ef))
* remove local option from global commands ([#1648](https://github.com/ipfs/js-ipfs/issues/1648)) ([8e963f9](https://github.com/ipfs/js-ipfs/commit/8e963f9))
* remove npm script ([df32ac4](https://github.com/ipfs/js-ipfs/commit/df32ac4))
* remove unused deps ([f7189fb](https://github.com/ipfs/js-ipfs/commit/f7189fb))
* use class is function on ipns ([#1617](https://github.com/ipfs/js-ipfs/issues/1617)) ([c240d49](https://github.com/ipfs/js-ipfs/commit/c240d49)), closes [js-peer-id#84](https://github.com/js-peer-id/issues/84) [interface-datastore#24](https://github.com/interface-datastore/issues/24) [#1615](https://github.com/ipfs/js-ipfs/issues/1615)
### Chores
* remove ipld formats re-export ([#1626](https://github.com/ipfs/js-ipfs/issues/1626)) ([3ee7b5e](https://github.com/ipfs/js-ipfs/commit/3ee7b5e))
* update to js-ipld 0.19 ([#1668](https://github.com/ipfs/js-ipfs/issues/1668)) ([74edafd](https://github.com/ipfs/js-ipfs/commit/74edafd))
### Features
* add support to pass config in the init cmd ([#1662](https://github.com/ipfs/js-ipfs/issues/1662)) ([588891c](https://github.com/ipfs/js-ipfs/commit/588891c))
* get Ping to work properly ([27d5a57](https://github.com/ipfs/js-ipfs/commit/27d5a57))
### BREAKING CHANGES
* dag-cbor nodes now represent links as CID objects
The API for [dag-cbor](https://github.com/ipld/js-ipld-dag-cbor) changed.
Links are no longer represented as JSON objects (`{"/": "base-encoded-cid"}`,
but as [CID objects](https://github.com/ipld/js-cid). `ipfs.dag.get()` and now always return links as CID objects. `ipfs.dag.put()` also expects links to be represented as CID objects. The old-style JSON objects representation is still
supported, but deprecated.
Prior to this change:
```js
const cid = new CID('QmXed8RihWcWFXRRmfSRG9yFjEbXNxu1bDwgCFAN8Dxcq5')
// Link as JSON object representation
const putCid = await ipfs.dag.put({link: {'/': cid.toBaseEncodedString()}})
const result = await ipfs.dag.get(putCid)
console.log(result.value)
```
Output:
```js
{ link:
{ '/':
} }
```
Now:
```js
const cid = new CID('QmXed8RihWcWFXRRmfSRG9yFjEbXNxu1bDwgCFAN8Dxcq5')
// Link as CID object
const putCid = await ipfs.dag.put({link: cid})
const result = await ipfs.dag.get(putCid)
console.log(result.value)
```
Output:
```js
{ link:
CID {
codec: 'dag-pb',
version: 0,
multihash:
} }
```
See https://github.com/ipld/ipld/issues/44 for more information on why this
change was made.
* remove `types.dagCBOR` and `types.dagPB` from public API
If you need the `ipld-dag-cbor` or `ipld-dag-pb` module in the Browser,
you need to bundle them yourself.
## [0.33.0-rc.3](https://github.com/ipfs/js-ipfs/compare/v0.33.0-rc.2...v0.33.0-rc.3) (2018-10-24)
## [0.33.0-rc.2](https://github.com/ipfs/js-ipfs/compare/v0.33.0-rc.1...v0.33.0-rc.2) (2018-10-23)
## [0.33.0-rc.1](https://github.com/ipfs/js-ipfs/compare/v0.32.3...v0.33.0-rc.1) (2018-10-19)
### Bug Fixes
* make ipfs.ping() options optional ([#1627](https://github.com/ipfs/js-ipfs/issues/1627)) ([08f06b6](https://github.com/ipfs/js-ipfs/commit/08f06b6)), closes [#1616](https://github.com/ipfs/js-ipfs/issues/1616)
### Features
* **gateway:** X-Ipfs-Path, Etag, Cache-Control, Suborigin ([#1537](https://github.com/ipfs/js-ipfs/issues/1537)) ([fc5bef7](https://github.com/ipfs/js-ipfs/commit/fc5bef7))
* add cid command ([#1560](https://github.com/ipfs/js-ipfs/issues/1560)) ([a22c791](https://github.com/ipfs/js-ipfs/commit/a22c791))
* show Web UI url in daemon output ([#1595](https://github.com/ipfs/js-ipfs/issues/1595)) ([9a82b05](https://github.com/ipfs/js-ipfs/commit/9a82b05))
* update to Web UI 2.0 ([#1647](https://github.com/ipfs/js-ipfs/issues/1647)) ([aea85aa](https://github.com/ipfs/js-ipfs/commit/aea85aa))
## [0.32.3](https://github.com/ipfs/js-ipfs/compare/v0.32.2...v0.32.3) (2018-09-28)
### Bug Fixes
* allow null/undefined options ([#1581](https://github.com/ipfs/js-ipfs/issues/1581)) ([c73bd2f](https://github.com/ipfs/js-ipfs/commit/c73bd2f)), closes [#1574](https://github.com/ipfs/js-ipfs/issues/1574)
* block.put with non default options ([#1600](https://github.com/ipfs/js-ipfs/issues/1600)) ([4ba0a24](https://github.com/ipfs/js-ipfs/commit/4ba0a24))
* ipns datastore get not found ([#1558](https://github.com/ipfs/js-ipfs/issues/1558)) ([4e99cf5](https://github.com/ipfs/js-ipfs/commit/4e99cf5))
* report correct size for raw dag nodes ([#1591](https://github.com/ipfs/js-ipfs/issues/1591)) ([549f2f6](https://github.com/ipfs/js-ipfs/commit/549f2f6)), closes [#1585](https://github.com/ipfs/js-ipfs/issues/1585)
* revert libp2p records being signed for ipns ([#1570](https://github.com/ipfs/js-ipfs/issues/1570)) ([855b3bd](https://github.com/ipfs/js-ipfs/commit/855b3bd))
## [0.32.2](https://github.com/ipfs/js-ipfs/compare/v0.32.1...v0.32.2) (2018-09-19)
### Bug Fixes
* coerce key gen size to number ([#1582](https://github.com/ipfs/js-ipfs/issues/1582)) ([25d820d](https://github.com/ipfs/js-ipfs/commit/25d820d))
## [0.32.1](https://github.com/ipfs/js-ipfs/compare/v0.32.0...v0.32.1) (2018-09-18)
### Bug Fixes
* add libp2p-crypto to deps list ([#1572](https://github.com/ipfs/js-ipfs/issues/1572)) ([7eaf571](https://github.com/ipfs/js-ipfs/commit/7eaf571)), closes [#1571](https://github.com/ipfs/js-ipfs/issues/1571)
* enable tests in node that were not being included ([#1499](https://github.com/ipfs/js-ipfs/issues/1499)) ([2585431](https://github.com/ipfs/js-ipfs/commit/2585431))
* fix `block rm` command ([#1576](https://github.com/ipfs/js-ipfs/issues/1576)) ([af30ea5](https://github.com/ipfs/js-ipfs/commit/af30ea5))
* mfs preload test ([#1551](https://github.com/ipfs/js-ipfs/issues/1551)) ([7c7a5a6](https://github.com/ipfs/js-ipfs/commit/7c7a5a6))
### Performance Improvements
* faster startup time ([#1542](https://github.com/ipfs/js-ipfs/issues/1542)) ([2790e6d](https://github.com/ipfs/js-ipfs/commit/2790e6d))
## [0.32.0](https://github.com/ipfs/js-ipfs/compare/v0.32.0-rc.2...v0.32.0) (2018-09-11)
### Bug Fixes
* ipns publish resolve option overwritten ([#1556](https://github.com/ipfs/js-ipfs/issues/1556)) ([ef7d2c8](https://github.com/ipfs/js-ipfs/commit/ef7d2c8))
### Features
* Added `ipfs.name.publish` and `ipfs.name.resolve`. This only works on your local node for the moment until the DHT lands. [API docs can be found here](https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/SPEC/NAME.md).
* Added `ipfs.resolve` API. Note that this is a partial implementation allowing you to resolve IPFS paths like `/ipfs/QmRootHash/path/to/file` to `/ipfs/QmFileHash`. It does not support IPNS yet.
* `ipfs.files.add*` now supports a `chunker` option, see [the API docs](https://github.com/ipfs/js-ipfs/blob/master/packages/interface-ipfs-core/SPEC/FILES.md#filesadd) for details
## [0.31.7](https://github.com/ipfs/js-ipfs/compare/v0.31.6...v0.31.7) (2018-08-20)
### Bug Fixes
* fails to start when preload disabled ([#1516](https://github.com/ipfs/js-ipfs/issues/1516)) ([511ab47](https://github.com/ipfs/js-ipfs/commit/511ab47)), closes [#1514](https://github.com/ipfs/js-ipfs/issues/1514)
* npm publishes examples folder ([#1513](https://github.com/ipfs/js-ipfs/issues/1513)) ([4a68ac1](https://github.com/ipfs/js-ipfs/commit/4a68ac1))
## [0.31.6](https://github.com/ipfs/js-ipfs/compare/v0.31.5...v0.31.6) (2018-08-17)
### Features
* adds data-encoding argument to control data encoding ([#1420](https://github.com/ipfs/js-ipfs/issues/1420)) ([1eb8485](https://github.com/ipfs/js-ipfs/commit/1eb8485))
## [0.31.5](https://github.com/ipfs/js-ipfs/compare/v0.31.4...v0.31.5) (2018-08-17)
### Bug Fixes
* add missing space after emoji ([5cde7c1](https://github.com/ipfs/js-ipfs/commit/5cde7c1))
* improper input validation ([#1506](https://github.com/ipfs/js-ipfs/issues/1506)) ([91a482b](https://github.com/ipfs/js-ipfs/commit/91a482b))
* object.patch.rmLink not working ([#1508](https://github.com/ipfs/js-ipfs/issues/1508)) ([afd3255](https://github.com/ipfs/js-ipfs/commit/afd3255))
* stub out call to fetch for ipfs.dns test in browser ([#1512](https://github.com/ipfs/js-ipfs/issues/1512)) ([86c3d81](https://github.com/ipfs/js-ipfs/commit/86c3d81))
## [0.31.4](https://github.com/ipfs/js-ipfs/compare/v0.31.3...v0.31.4) (2018-08-09)
### Bug Fixes
* consistent badge style in docs ([#1494](https://github.com/ipfs/js-ipfs/issues/1494)) ([4a72e23](https://github.com/ipfs/js-ipfs/commit/4a72e23))
* files.ls and files.read*Stream tests ([#1493](https://github.com/ipfs/js-ipfs/issues/1493)) ([a0bc79b](https://github.com/ipfs/js-ipfs/commit/a0bc79b))
## [0.31.3](https://github.com/ipfs/js-ipfs/compare/v0.31.2...v0.31.3) (2018-08-09)
### Bug Fixes
* failing tests in master ([#1488](https://github.com/ipfs/js-ipfs/issues/1488)) ([e607560](https://github.com/ipfs/js-ipfs/commit/e607560))
* **dag:** check dag.put options for plain object ([#1480](https://github.com/ipfs/js-ipfs/issues/1480)) ([d0b671b](https://github.com/ipfs/js-ipfs/commit/d0b671b)), closes [#1479](https://github.com/ipfs/js-ipfs/issues/1479)
* **dht:** allow for options object in `findProvs()` API ([#1457](https://github.com/ipfs/js-ipfs/issues/1457)) ([99911b1](https://github.com/ipfs/js-ipfs/commit/99911b1)), closes [#1322](https://github.com/ipfs/js-ipfs/issues/1322)
## [0.31.2](https://github.com/ipfs/js-ipfs/compare/v0.31.1...v0.31.2) (2018-08-02)
### Bug Fixes
* fix content-type by doing a fall-back using extensions ([#1482](https://github.com/ipfs/js-ipfs/issues/1482)) ([d528b3f](https://github.com/ipfs/js-ipfs/commit/d528b3f))
## [0.31.1](https://github.com/ipfs/js-ipfs/compare/v0.31.0...v0.31.1) (2018-07-29)
### Bug Fixes
* logo link ([a9219ad](https://github.com/ipfs/js-ipfs/commit/a9219ad))
* XMLHTTPRequest is deprecated and unavailable in service workers ([#1478](https://github.com/ipfs/js-ipfs/issues/1478)) ([7d6f0ca](https://github.com/ipfs/js-ipfs/commit/7d6f0ca))
## [0.31.0](https://github.com/ipfs/js-ipfs/compare/v0.30.1...v0.31.0) (2018-07-29)
### Bug Fixes
* emit boot error only once ([#1472](https://github.com/ipfs/js-ipfs/issues/1472)) ([45b80a0](https://github.com/ipfs/js-ipfs/commit/45b80a0))
### Features
* preload content ([#1464](https://github.com/ipfs/js-ipfs/issues/1464)) ([bffe080](https://github.com/ipfs/js-ipfs/commit/bffe080)), closes [#1459](https://github.com/ipfs/js-ipfs/issues/1459)
* preload on content fetch requests ([#1475](https://github.com/ipfs/js-ipfs/issues/1475)) ([649b755](https://github.com/ipfs/js-ipfs/commit/649b755)), closes [#1473](https://github.com/ipfs/js-ipfs/issues/1473)
* remove decomissioned bootstrappers ([e3868f4](https://github.com/ipfs/js-ipfs/commit/e3868f4))
* rm decomissioned bootstrappers - nodejs ([90e9f68](https://github.com/ipfs/js-ipfs/commit/90e9f68))
* support --raw-leaves ([#1454](https://github.com/ipfs/js-ipfs/issues/1454)) ([1f63e8c](https://github.com/ipfs/js-ipfs/commit/1f63e8c))
### Reverts
* docs: add migration note about upgrading from < 0.30.0 ([#1450](https://github.com/ipfs/js-ipfs/issues/1450)) ([#1456](https://github.com/ipfs/js-ipfs/issues/1456)) ([f4344b0](https://github.com/ipfs/js-ipfs/commit/f4344b0))
## [0.30.1](https://github.com/ipfs/js-ipfs/compare/v0.30.0...v0.30.1) (2018-07-17)
### Bug Fixes
* aegir docs fails if outer funtion is called pin ([#1429](https://github.com/ipfs/js-ipfs/issues/1429)) ([a08a17d](https://github.com/ipfs/js-ipfs/commit/a08a17d))
* double pre start ([#1437](https://github.com/ipfs/js-ipfs/issues/1437)) ([e6ad63e](https://github.com/ipfs/js-ipfs/commit/e6ad63e))
* fixing circuit-relaying example ([#1443](https://github.com/ipfs/js-ipfs/issues/1443)) ([a681fc5](https://github.com/ipfs/js-ipfs/commit/a681fc5)), closes [#1423](https://github.com/ipfs/js-ipfs/issues/1423)
## [0.30.0](https://github.com/ipfs/js-ipfs/compare/v0.29.3...v0.30.0) (2018-07-09)
### Bug Fixes
* allow put empty block & add X-Stream-Output header on get ([#1408](https://github.com/ipfs/js-ipfs/issues/1408)) ([52f7aa7](https://github.com/ipfs/js-ipfs/commit/52f7aa7))
* broken contributing links ([#1386](https://github.com/ipfs/js-ipfs/issues/1386)) ([cd449ff](https://github.com/ipfs/js-ipfs/commit/cd449ff))
* do not stringify output of object data ([#1398](https://github.com/ipfs/js-ipfs/issues/1398)) ([4e51a69](https://github.com/ipfs/js-ipfs/commit/4e51a69))
* **dag:** fix default hash algorithm for put() api ([#1419](https://github.com/ipfs/js-ipfs/issues/1419)) ([1a36375](https://github.com/ipfs/js-ipfs/commit/1a36375))
* **dag:** make options in `put` API optional ([#1415](https://github.com/ipfs/js-ipfs/issues/1415)) ([d299ed7](https://github.com/ipfs/js-ipfs/commit/d299ed7)), closes [#1395](https://github.com/ipfs/js-ipfs/issues/1395)
* **tests:** loosen assertion for bitswap.stat test ([#1404](https://github.com/ipfs/js-ipfs/issues/1404)) ([4290256](https://github.com/ipfs/js-ipfs/commit/4290256))
* update hlsjs-ipfs-loader version ([#1422](https://github.com/ipfs/js-ipfs/issues/1422)) ([6b14812](https://github.com/ipfs/js-ipfs/commit/6b14812))
### Features
* (BREAKING CHANGE) new libp2p configuration ([#1401](https://github.com/ipfs/js-ipfs/issues/1401)) ([9c60909](https://github.com/ipfs/js-ipfs/commit/9c60909))
* expose libp2p connection manager configuration options ([#1410](https://github.com/ipfs/js-ipfs/issues/1410)) ([2615f76](https://github.com/ipfs/js-ipfs/commit/2615f76))
* implement bitswap.wantlist peerid and bitswap.unwant ([#1349](https://github.com/ipfs/js-ipfs/issues/1349)) ([45b705d](https://github.com/ipfs/js-ipfs/commit/45b705d))
* mfs implementation ([#1360](https://github.com/ipfs/js-ipfs/issues/1360)) ([871d24e](https://github.com/ipfs/js-ipfs/commit/871d24e)), closes [#1425](https://github.com/ipfs/js-ipfs/issues/1425)
* modular interface tests ([#1389](https://github.com/ipfs/js-ipfs/issues/1389)) ([18888be](https://github.com/ipfs/js-ipfs/commit/18888be))
* pin API ([#1045](https://github.com/ipfs/js-ipfs/issues/1045)) ([2a5cc5e](https://github.com/ipfs/js-ipfs/commit/2a5cc5e)), closes [#1249](https://github.com/ipfs/js-ipfs/issues/1249)
### Performance Improvements
* use lodash ([#1414](https://github.com/ipfs/js-ipfs/issues/1414)) ([5637330](https://github.com/ipfs/js-ipfs/commit/5637330))
### BREAKING CHANGES
* libp2p configuration has changed
* old: `libp2p.modules.discovery`
* new: `libp2p.modules.peerDiscovery`
License: MIT
Signed-off-by: David Dias
License: MIT
Signed-off-by: Alan Shaw
## [0.29.3](https://github.com/ipfs/js-ipfs/compare/v0.29.2...v0.29.3) (2018-06-04)
### Bug Fixes
* **repo:** do not hang on calls to repo gc ([9fff46f](https://github.com/ipfs/js-ipfs/commit/9fff46f))
## [0.29.2](https://github.com/ipfs/js-ipfs/compare/v0.29.1...v0.29.2) (2018-06-01)
### Bug Fixes
* adds missing breaking changes for 0.29 to changelog ([#1370](https://github.com/ipfs/js-ipfs/issues/1370)) ([61ba99e](https://github.com/ipfs/js-ipfs/commit/61ba99e))
* dont fail on uninitialized repo ([#1374](https://github.com/ipfs/js-ipfs/issues/1374)) ([6f0a95b](https://github.com/ipfs/js-ipfs/commit/6f0a95b))
## [0.29.1](https://github.com/ipfs/js-ipfs/compare/v0.29.0...v0.29.1) (2018-05-30)
### Bug Fixes
* check for repo uninitialized error ([dcf5ea5](https://github.com/ipfs/js-ipfs/commit/dcf5ea5))
* update ipfs-repo errors require ([4d1318d](https://github.com/ipfs/js-ipfs/commit/4d1318d))
## [0.29.0](https://github.com/ipfs/js-ipfs/compare/v0.28.2...v0.29.0) (2018-05-29)
### Bug Fixes
* Add ipfs path to cli help ([64c3bfb](https://github.com/ipfs/js-ipfs/commit/64c3bfb))
* change ^ to ~ on 0.x.x deps ([#1345](https://github.com/ipfs/js-ipfs/issues/1345)) ([de95989](https://github.com/ipfs/js-ipfs/commit/de95989))
* change default config from JSON file to JS module to prevent having it doubly used ([#1324](https://github.com/ipfs/js-ipfs/issues/1324)) ([c3d2d1e](https://github.com/ipfs/js-ipfs/commit/c3d2d1e)), closes [#1316](https://github.com/ipfs/js-ipfs/issues/1316)
* changes peer prop in return value from swarm.peers to be a PeerId ([#1252](https://github.com/ipfs/js-ipfs/issues/1252)) ([e174866](https://github.com/ipfs/js-ipfs/commit/e174866))
* configure webpack to not use esmodules in dependencies ([4486acc](https://github.com/ipfs/js-ipfs/commit/4486acc))
* Display error when using unkown cli option ([a849d2f](https://github.com/ipfs/js-ipfs/commit/a849d2f))
* docker init script sed in non existent file ([#1246](https://github.com/ipfs/js-ipfs/issues/1246)) ([75d47c3](https://github.com/ipfs/js-ipfs/commit/75d47c3))
* files.add with pull streams ([0e601a7](https://github.com/ipfs/js-ipfs/commit/0e601a7))
* make pubsub.unsubscribe async and alter pubsub.subscribe signature ([a115829](https://github.com/ipfs/js-ipfs/commit/a115829))
* remove unused var ([#1273](https://github.com/ipfs/js-ipfs/issues/1273)) ([c1e8db1](https://github.com/ipfs/js-ipfs/commit/c1e8db1))
* typo ([#1367](https://github.com/ipfs/js-ipfs/issues/1367)) ([2679129](https://github.com/ipfs/js-ipfs/commit/2679129))
* use async/setImmediate vs process.nextTick ([af55608](https://github.com/ipfs/js-ipfs/commit/af55608))
### Features
* .stats.bw* - Bandwidth Stats ([#1230](https://github.com/ipfs/js-ipfs/issues/1230)) ([9694925](https://github.com/ipfs/js-ipfs/commit/9694925))
* add ability to files.cat with a cid instance ([2e332c8](https://github.com/ipfs/js-ipfs/commit/2e332c8))
* Add support for specifying hash algorithms in files.add ([a2954cb](https://github.com/ipfs/js-ipfs/commit/a2954cb))
* allow dht to be enabled via cli arg ([#1340](https://github.com/ipfs/js-ipfs/issues/1340)) ([7bb838f](https://github.com/ipfs/js-ipfs/commit/7bb838f))
* Allows for byte offsets when using ipfs.files.cat and friends to request slices of files ([a93971a](https://github.com/ipfs/js-ipfs/commit/a93971a))
* Circuit Relay ([#1063](https://github.com/ipfs/js-ipfs/issues/1063)) ([f7eaa43](https://github.com/ipfs/js-ipfs/commit/f7eaa43))
* cli: add IPFS_PATH info to init command help ([#1274](https://github.com/ipfs/js-ipfs/issues/1274)) ([e189b72](https://github.com/ipfs/js-ipfs/commit/e189b72))
* handle SIGHUP ([7a817cf](https://github.com/ipfs/js-ipfs/commit/7a817cf))
* ipfs.ping cli, http-api and core ([#1342](https://github.com/ipfs/js-ipfs/issues/1342)) ([b8171b1](https://github.com/ipfs/js-ipfs/commit/b8171b1))
* jsipfs add --only-hash ([#1233](https://github.com/ipfs/js-ipfs/issues/1233)) ([#1266](https://github.com/ipfs/js-ipfs/issues/1266)) ([bddc5b4](https://github.com/ipfs/js-ipfs/commit/bddc5b4))
* Provide access to bundled libraries when in browser ([#1297](https://github.com/ipfs/js-ipfs/issues/1297)) ([4905c2d](https://github.com/ipfs/js-ipfs/commit/4905c2d))
* use class-is for type checks ([5b2cf8c](https://github.com/ipfs/js-ipfs/commit/5b2cf8c))
* wrap with directory ([#1329](https://github.com/ipfs/js-ipfs/issues/1329)) ([47285a7](https://github.com/ipfs/js-ipfs/commit/47285a7))
### Performance Improvements
* **cli:** load only sub-system modules and inline require ipfs ([3820be0](https://github.com/ipfs/js-ipfs/commit/3820be0))
### BREAKING CHANGES
1. Argument order for `pubsub.subscribe` has changed:
* Old: `pubsub.subscribe(topic, [options], handler, [callback]): Promise`
* New: `pubsub.subscribe(topic, handler, [options], [callback]): Promise`
2. The `pubsub.unsubscribe` method has become async meaning that it now takes a callback or returns a promise:
* Old: `pubsub.unsubscribe(topic, handler): undefined`
* New: `pubsub.unsubscribe(topic, handler, [callback]): Promise`
3. Property names on response objects for `ping` are now lowered:
* Old: `{ Success, Time, Text }`
* New: `{ success, time, text }`
4. In the CLI, `jsipfs object data` no longer returns a newline after the end of the returned data
## [0.28.2](https://github.com/ipfs/js-ipfs/compare/v0.28.1...v0.28.2) (2018-03-14)
### Bug Fixes
* match error if repo doesnt exist ([#1262](https://github.com/ipfs/js-ipfs/issues/1262)) ([aea69d3](https://github.com/ipfs/js-ipfs/commit/aea69d3))
* reinstates the non local block check in dht.provide ([#1250](https://github.com/ipfs/js-ipfs/issues/1250)) ([5b736a8](https://github.com/ipfs/js-ipfs/commit/5b736a8))
### Features
* add config validation ([#1239](https://github.com/ipfs/js-ipfs/issues/1239)) ([a32dce7](https://github.com/ipfs/js-ipfs/commit/a32dce7))
## [0.28.1](https://github.com/ipfs/js-ipfs/compare/v0.28.0...v0.28.1) (2018-03-09)
### Bug Fixes
* **gateway:** catch stream2 error ([#1243](https://github.com/ipfs/js-ipfs/issues/1243)) ([5b40b41](https://github.com/ipfs/js-ipfs/commit/5b40b41))
* accept objects in file.add ([#1257](https://github.com/ipfs/js-ipfs/issues/1257)) ([d32dad9](https://github.com/ipfs/js-ipfs/commit/d32dad9))
## [0.28.0](https://github.com/ipfs/js-ipfs/compare/v0.27.7...v0.28.0) (2018-03-01)
### Bug Fixes
* **cli:** show help for subcommands ([8c63f8f](https://github.com/ipfs/js-ipfs/commit/8c63f8f))
* (cli/init) use cross-platform path separator ([bbb7cc5](https://github.com/ipfs/js-ipfs/commit/bbb7cc5))
* **dag:** print data in a readable way if it is JSON ([42545dc](https://github.com/ipfs/js-ipfs/commit/42545dc))
* bootstrap ([d527b45](https://github.com/ipfs/js-ipfs/commit/d527b45))
* now properly fix bootstrap in core ([9f39a6f](https://github.com/ipfs/js-ipfs/commit/9f39a6f))
* Remove scape characteres from error message. ([68e7b5a](https://github.com/ipfs/js-ipfs/commit/68e7b5a))
* Return swarm http errors as json ([d3a0ae1](https://github.com/ipfs/js-ipfs/commit/d3a0ae1)), closes [#1176](https://github.com/ipfs/js-ipfs/issues/1176)
* stats tests ([a0fd355](https://github.com/ipfs/js-ipfs/commit/a0fd355))
* use "ipld" instead of "ipld-resolver" ([e7f0432](https://github.com/ipfs/js-ipfs/commit/e7f0432))
### Features
* `ipfs version` flags + `ipfs repo version` ([#1181](https://github.com/ipfs/js-ipfs/issues/1181)) ([#1188](https://github.com/ipfs/js-ipfs/issues/1188)) ([494da7f](https://github.com/ipfs/js-ipfs/commit/494da7f))
* Add /ip6 addresses to bootstrap ([3bca165](https://github.com/ipfs/js-ipfs/commit/3bca165)), closes [#706](https://github.com/ipfs/js-ipfs/issues/706)
* all pubsub tests passing with libp2p pubsub ([6fe015f](https://github.com/ipfs/js-ipfs/commit/6fe015f))
* Bootstrap API compliance ([#1218](https://github.com/ipfs/js-ipfs/issues/1218)) ([9a445d1](https://github.com/ipfs/js-ipfs/commit/9a445d1))
* Implementation of the ipfs.key API ([#1133](https://github.com/ipfs/js-ipfs/issues/1133)) ([d945fce](https://github.com/ipfs/js-ipfs/commit/d945fce))
* improved multiaddr validation. ([d9744a1](https://github.com/ipfs/js-ipfs/commit/d9744a1))
* ipfs shutdown ([#1200](https://github.com/ipfs/js-ipfs/issues/1200)) ([95365fa](https://github.com/ipfs/js-ipfs/commit/95365fa))
* jsipfs ls -r (Recursive list directory) ([#1222](https://github.com/ipfs/js-ipfs/issues/1222)) ([0f1e00f](https://github.com/ipfs/js-ipfs/commit/0f1e00f))
* latest libp2p + other deps. Fix bugs in tests along the way ([4b79066](https://github.com/ipfs/js-ipfs/commit/4b79066))
* reworking tests with new ipfsd-ctl ([#1167](https://github.com/ipfs/js-ipfs/issues/1167)) ([d16a129](https://github.com/ipfs/js-ipfs/commit/d16a129))
* stats API (stats.bitswap and stats.repo) ([#1198](https://github.com/ipfs/js-ipfs/issues/1198)) ([905bdc0](https://github.com/ipfs/js-ipfs/commit/905bdc0))
* support Jenkins ([bc66e9f](https://github.com/ipfs/js-ipfs/commit/bc66e9f))
* use PubSub API directly from libp2p ([6b9fc95](https://github.com/ipfs/js-ipfs/commit/6b9fc95))
* use reduces keysize ([#1232](https://github.com/ipfs/js-ipfs/issues/1232)) ([7f69628](https://github.com/ipfs/js-ipfs/commit/7f69628))
## [0.27.7](https://github.com/ipfs/js-ipfs/compare/v0.27.6...v0.27.7) (2018-01-16)
### Features
* /api/v0/dns ([#1172](https://github.com/ipfs/js-ipfs/issues/1172)) ([639024c](https://github.com/ipfs/js-ipfs/commit/639024c))
## [0.27.6](https://github.com/ipfs/js-ipfs/compare/v0.27.5...v0.27.6) (2018-01-07)
### Bug Fixes
* cli files on Windows ([#1159](https://github.com/ipfs/js-ipfs/issues/1159)) ([1b98fa1](https://github.com/ipfs/js-ipfs/commit/1b98fa1))
## [0.27.5](https://github.com/ipfs/js-ipfs/compare/v0.27.4...v0.27.5) (2017-12-18)
### Bug Fixes
* cat: test file existence after filtering ([#1148](https://github.com/ipfs/js-ipfs/issues/1148)) ([34f28ef](https://github.com/ipfs/js-ipfs/commit/34f28ef)), closes [#1142](https://github.com/ipfs/js-ipfs/issues/1142)
* ipfs.ls: allow any depth ([#1152](https://github.com/ipfs/js-ipfs/issues/1152)) ([279af78](https://github.com/ipfs/js-ipfs/commit/279af78)), closes [#1079](https://github.com/ipfs/js-ipfs/issues/1079)
* use new bitswap stats ([#1151](https://github.com/ipfs/js-ipfs/issues/1151)) ([e223888](https://github.com/ipfs/js-ipfs/commit/e223888))
* **files.add:** directory with odd name ([#1155](https://github.com/ipfs/js-ipfs/issues/1155)) ([058c674](https://github.com/ipfs/js-ipfs/commit/058c674))
## [0.27.4](https://github.com/ipfs/js-ipfs/compare/v0.27.3...v0.27.4) (2017-12-13)
### Bug Fixes
* files.cat: detect and handle rrors when unknown path and cat dir ([#1143](https://github.com/ipfs/js-ipfs/issues/1143)) ([120d291](https://github.com/ipfs/js-ipfs/commit/120d291))
* fix bug introduced by 1143 ([#1146](https://github.com/ipfs/js-ipfs/issues/1146)) ([12cdc08](https://github.com/ipfs/js-ipfs/commit/12cdc08))
## [0.27.3](https://github.com/ipfs/js-ipfs/compare/v0.27.2...v0.27.3) (2017-12-10)
### Bug Fixes
* config handler should check if value is null ([#1134](https://github.com/ipfs/js-ipfs/issues/1134)) ([0444c42](https://github.com/ipfs/js-ipfs/commit/0444c42))
* **pubsub:** subscribe promises ([#1141](https://github.com/ipfs/js-ipfs/issues/1141)) ([558017d](https://github.com/ipfs/js-ipfs/commit/558017d))
## [0.27.2](https://github.com/ipfs/js-ipfs/compare/v0.27.1...v0.27.2) (2017-12-09)
## [0.27.1](https://github.com/ipfs/js-ipfs/compare/v0.27.0...v0.27.1) (2017-12-07)
### Bug Fixes
* **pubsub.peers:** remove the requirement for a topic ([#1125](https://github.com/ipfs/js-ipfs/issues/1125)) ([5601c26](https://github.com/ipfs/js-ipfs/commit/5601c26))
## [0.27.0](https://github.com/ipfs/js-ipfs/compare/v0.26.0...v0.27.0) (2017-12-04)
### Bug Fixes
* fix the welcome message and throw error when trying to cat a non-exis… ([#1032](https://github.com/ipfs/js-ipfs/issues/1032)) ([25fb390](https://github.com/ipfs/js-ipfs/commit/25fb390)), closes [#1031](https://github.com/ipfs/js-ipfs/issues/1031)
* make offline error retain stack ([#1056](https://github.com/ipfs/js-ipfs/issues/1056)) ([dce6a49](https://github.com/ipfs/js-ipfs/commit/dce6a49))
* pre 1.0.0 deps should be always installed with ~ and not ^ ([c672af7](https://github.com/ipfs/js-ipfs/commit/c672af7))
* progress bar flakiness ([#1042](https://github.com/ipfs/js-ipfs/issues/1042)) ([d7732c3](https://github.com/ipfs/js-ipfs/commit/d7732c3))
* promisify .block (get, put, rm, stat) ([#1085](https://github.com/ipfs/js-ipfs/issues/1085)) ([cafa52b](https://github.com/ipfs/js-ipfs/commit/cafa52b))
* **files.add:** glob needs a POSIX path ([#1108](https://github.com/ipfs/js-ipfs/issues/1108)) ([9c29a23](https://github.com/ipfs/js-ipfs/commit/9c29a23))
* promisify node.stop ([#1082](https://github.com/ipfs/js-ipfs/issues/1082)) ([9b385ae](https://github.com/ipfs/js-ipfs/commit/9b385ae))
* pubsub message fields ([#1077](https://github.com/ipfs/js-ipfs/issues/1077)) ([9de6f4c](https://github.com/ipfs/js-ipfs/commit/9de6f4c))
* removed error handler that was hiding errors ([#1120](https://github.com/ipfs/js-ipfs/issues/1120)) ([58ded8d](https://github.com/ipfs/js-ipfs/commit/58ded8d))
* Typo ([#1044](https://github.com/ipfs/js-ipfs/issues/1044)) ([179b6a4](https://github.com/ipfs/js-ipfs/commit/179b6a4))
* update *-star multiaddrs to explicity say that they need tcp and a port ([#1117](https://github.com/ipfs/js-ipfs/issues/1117)) ([9eda8a8](https://github.com/ipfs/js-ipfs/commit/9eda8a8))
### Features
* accept additional transports ([6613aa6](https://github.com/ipfs/js-ipfs/commit/6613aa6))
* add circuit relay and aegir 12 (+ big refactor) ([104ef1e](https://github.com/ipfs/js-ipfs/commit/104ef1e))
* add WebUI Path ([#1124](https://github.com/ipfs/js-ipfs/issues/1124)) ([8041b48](https://github.com/ipfs/js-ipfs/commit/8041b48))
* adding appveyor support ([#1054](https://github.com/ipfs/js-ipfs/issues/1054)) ([b92bdfe](https://github.com/ipfs/js-ipfs/commit/b92bdfe))
* agent version with package number ([#1121](https://github.com/ipfs/js-ipfs/issues/1121)) ([550f955](https://github.com/ipfs/js-ipfs/commit/550f955))
* cli --api option ([#1087](https://github.com/ipfs/js-ipfs/issues/1087)) ([1b1fa05](https://github.com/ipfs/js-ipfs/commit/1b1fa05))
* complete PubSub implementation ([ac95601](https://github.com/ipfs/js-ipfs/commit/ac95601))
* implement "ipfs file ls" ([#1078](https://github.com/ipfs/js-ipfs/issues/1078)) ([6db3fb8](https://github.com/ipfs/js-ipfs/commit/6db3fb8))
* implementing the new streaming interfaces ([#1086](https://github.com/ipfs/js-ipfs/issues/1086)) ([2c4b8b3](https://github.com/ipfs/js-ipfs/commit/2c4b8b3))
* ipfs.ls ([#1073](https://github.com/ipfs/js-ipfs/issues/1073)) ([35687cb](https://github.com/ipfs/js-ipfs/commit/35687cb))
* make js-ipfs daemon stop with same SIG as go-ipfs ([#1067](https://github.com/ipfs/js-ipfs/issues/1067)) ([7dd4e01](https://github.com/ipfs/js-ipfs/commit/7dd4e01))
* WebSocketStar ([#1090](https://github.com/ipfs/js-ipfs/issues/1090)) ([33e9949](https://github.com/ipfs/js-ipfs/commit/33e9949))
* windows interop ([#1065](https://github.com/ipfs/js-ipfs/issues/1065)) ([d8197f9](https://github.com/ipfs/js-ipfs/commit/d8197f9))
## [0.26.0](https://github.com/ipfs/js-ipfs/compare/v0.25.4...v0.26.0) (2017-09-13)
### Bug Fixes
* strips trailing slash from path ([#985](https://github.com/ipfs/js-ipfs/issues/985)) ([bfc58d6](https://github.com/ipfs/js-ipfs/commit/bfc58d6))
### Features
* Add --cid-version option to ipfs files add + decodeURIComponent for file and directory names ([7544b7b](https://github.com/ipfs/js-ipfs/commit/7544b7b))
* add gateway to ipfs daemon ([9f2006e](https://github.com/ipfs/js-ipfs/commit/9f2006e)), closes [#1006](https://github.com/ipfs/js-ipfs/issues/1006) [#1008](https://github.com/ipfs/js-ipfs/issues/1008) [#1009](https://github.com/ipfs/js-ipfs/issues/1009)
* adds quiet flags ([#1001](https://github.com/ipfs/js-ipfs/issues/1001)) ([d21b492](https://github.com/ipfs/js-ipfs/commit/d21b492))
* complete the migration to p2p-webrtc-star ([#984](https://github.com/ipfs/js-ipfs/issues/984)) ([1e5dd2c](https://github.com/ipfs/js-ipfs/commit/1e5dd2c))
## [0.25.4](https://github.com/ipfs/js-ipfs/compare/v0.25.3...v0.25.4) (2017-09-01)
### Features
* add multiaddrs for bootstrapers gateway ([a15bee9](https://github.com/ipfs/js-ipfs/commit/a15bee9))
## [0.25.3](https://github.com/ipfs/js-ipfs/compare/v0.25.2...v0.25.3) (2017-09-01)
### Bug Fixes
* config, dangling comma ([4eb63c5](https://github.com/ipfs/js-ipfs/commit/4eb63c5))
* only show connected addrs for peers in swarm.peers ([d939323](https://github.com/ipfs/js-ipfs/commit/d939323))
* remove shutdown bootstrapers from bootstrappers list ([5ec27a3](https://github.com/ipfs/js-ipfs/commit/5ec27a3))
### Features
* add instrumentation ([8f0254e](https://github.com/ipfs/js-ipfs/commit/8f0254e))
## [0.25.2](https://github.com/ipfs/js-ipfs/compare/v0.25.1...v0.25.2) (2017-08-26)
## [0.25.1](https://github.com/ipfs/js-ipfs/compare/v0.25.0...v0.25.1) (2017-07-26)
### Bug Fixes
* js-ipfs daemon config params ([#914](https://github.com/ipfs/js-ipfs/issues/914)) ([e00b96f](https://github.com/ipfs/js-ipfs/commit/e00b96f)), closes [#868](https://github.com/ipfs/js-ipfs/issues/868)
* remove non existent commands ([#925](https://github.com/ipfs/js-ipfs/issues/925)) ([b7e8e88](https://github.com/ipfs/js-ipfs/commit/b7e8e88))
* stream issue, do not use isstream, use is-stream ([#937](https://github.com/ipfs/js-ipfs/issues/937)) ([da66b1f](https://github.com/ipfs/js-ipfs/commit/da66b1f))
### Features
* new print func for the CLI ([#931](https://github.com/ipfs/js-ipfs/issues/931)) ([a5e75e0](https://github.com/ipfs/js-ipfs/commit/a5e75e0))
* no more need for webcrypto-ossl ([bc8ffee](https://github.com/ipfs/js-ipfs/commit/bc8ffee))
## [0.25.0](https://github.com/ipfs/js-ipfs/compare/v0.24.1...v0.25.0) (2017-07-12)
### Bug Fixes
* **bootstrap:add:** prevent duplicate inserts ([#893](https://github.com/ipfs/js-ipfs/issues/893)) ([ce504cd](https://github.com/ipfs/js-ipfs/commit/ce504cd))
* **swarm:** move isConnected filter from addrs to peers ([#901](https://github.com/ipfs/js-ipfs/issues/901)) ([e2f371b](https://github.com/ipfs/js-ipfs/commit/e2f371b))
* circle ci, thanks victor! ([b074966](https://github.com/ipfs/js-ipfs/commit/b074966))
* do not let lodash mess with libp2p modules ([1f68b9b](https://github.com/ipfs/js-ipfs/commit/1f68b9b))
* is online is only online if libp2p is online ([#891](https://github.com/ipfs/js-ipfs/issues/891)) ([8b0f996](https://github.com/ipfs/js-ipfs/commit/8b0f996))
* issue [#905](https://github.com/ipfs/js-ipfs/issues/905) ([#906](https://github.com/ipfs/js-ipfs/issues/906)) ([cbcf90e](https://github.com/ipfs/js-ipfs/commit/cbcf90e))
* setImmediate polyfilled in node.id() ([#909](https://github.com/ipfs/js-ipfs/issues/909)) ([ebaf9a0](https://github.com/ipfs/js-ipfs/commit/ebaf9a0))
* succeed when stopping already stopped ([74f3185](https://github.com/ipfs/js-ipfs/commit/74f3185))
### Features
* adapted to new ipfs-repo API ([#887](https://github.com/ipfs/js-ipfs/issues/887)) ([4e39d2c](https://github.com/ipfs/js-ipfs/commit/4e39d2c))
* block get pipe fix ([#903](https://github.com/ipfs/js-ipfs/issues/903)) ([8063f6b](https://github.com/ipfs/js-ipfs/commit/8063f6b))
## [0.24.1](https://github.com/ipfs/js-ipfs/compare/0.24.1...v0.24.1) (2017-05-29)
## [0.24.0](https://github.com/ipfs/js-ipfs/compare/v0.23.1...v0.24.0) (2017-05-24)
### Bug Fixes
* cli flag typos ([c5bb0b9](https://github.com/ipfs/js-ipfs/commit/c5bb0b9))
* example, now files from datatransfer is a FileList which is not an array ([d7c9eec](https://github.com/ipfs/js-ipfs/commit/d7c9eec))
* issue-858 ([481933a](https://github.com/ipfs/js-ipfs/commit/481933a))
* last touches for dns websockets bootstrapers ([3b680a7](https://github.com/ipfs/js-ipfs/commit/3b680a7))
* linting ([68ee42e](https://github.com/ipfs/js-ipfs/commit/68ee42e))
* make start an async event ([78ba1e8](https://github.com/ipfs/js-ipfs/commit/78ba1e8))
* missing import ([6aa914d](https://github.com/ipfs/js-ipfs/commit/6aa914d))
* options to the HTTP API ([f1eb595](https://github.com/ipfs/js-ipfs/commit/f1eb595))
* removed hard-coded timeout on test and liting fixes ([0a3bbcb](https://github.com/ipfs/js-ipfs/commit/0a3bbcb))
* run webworker tests ([23c84f6](https://github.com/ipfs/js-ipfs/commit/23c84f6))
* **object.get:** treat ipfs hash strings as default base58 encoded ([7b3caef](https://github.com/ipfs/js-ipfs/commit/7b3caef))
* update bootstrapers ([7e7d9eb](https://github.com/ipfs/js-ipfs/commit/7e7d9eb))
### Features
* add dns ws bootstrappers ([a856578](https://github.com/ipfs/js-ipfs/commit/a856578))
* add WebRTC by default as a multiaddr ([4ea1571](https://github.com/ipfs/js-ipfs/commit/4ea1571))
* add websocket bootstrapers to the config ([602d033](https://github.com/ipfs/js-ipfs/commit/602d033))
* DHT integration PART I ([860165c](https://github.com/ipfs/js-ipfs/commit/860165c))
* new libp2p-api ([7bf75d1](https://github.com/ipfs/js-ipfs/commit/7bf75d1))
* update to new libp2p events for peers ([ca88706](https://github.com/ipfs/js-ipfs/commit/ca88706))
* update to the latest libp2p ([aca4297](https://github.com/ipfs/js-ipfs/commit/aca4297))
## [0.23.1](https://github.com/ipfs/js-ipfs/compare/v0.23.0...v0.23.1) (2017-03-27)
### Bug Fixes
* added backpressure to the add stream ([#810](https://github.com/ipfs/js-ipfs/issues/810)) ([31dbabc](https://github.com/ipfs/js-ipfs/commit/31dbabc))
## [0.23.0](https://github.com/ipfs/js-ipfs/compare/v0.22.1...v0.23.0) (2017-03-24)
### Bug Fixes
* **files.add:** error on invalid input ([#782](https://github.com/ipfs/js-ipfs/issues/782)) ([c851ca0](https://github.com/ipfs/js-ipfs/commit/c851ca0))
* give the daemon time to spawn ([2bf32cd](https://github.com/ipfs/js-ipfs/commit/2bf32cd))
* linting on transfer-files example ([f876171](https://github.com/ipfs/js-ipfs/commit/f876171))
* offer an init event to monitor when repo is there and avoid setTimeout ([c4130b9](https://github.com/ipfs/js-ipfs/commit/c4130b9))
* pull-stream-to-stream replaced with duplex stream ([#809](https://github.com/ipfs/js-ipfs/issues/809)) ([4b064a1](https://github.com/ipfs/js-ipfs/commit/4b064a1))
### Features
* bootstrap is enabled by default now ([64cde5d](https://github.com/ipfs/js-ipfs/commit/64cde5d))
* bootstrap is enabled by default now ([2642417](https://github.com/ipfs/js-ipfs/commit/2642417))
* datastore, ipfs-block and all the deps that were updated ([68d92b6](https://github.com/ipfs/js-ipfs/commit/68d92b6))
* no need anymore to append ipfs/Qmhash to webrtc-star multiaddrs ([a77ae3c](https://github.com/ipfs/js-ipfs/commit/a77ae3c))
## [0.22.1](https://github.com/ipfs/js-ipfs/compare/v0.22.0...v0.22.1) (2017-02-24)
### Bug Fixes
* interop tests with multiplex passing ([cb109fc](https://github.com/ipfs/js-ipfs/commit/cb109fc))
### Features
* **core:** allow IPFS object to be created without supplying configOpts ([f620d71](https://github.com/ipfs/js-ipfs/commit/f620d71))
* **deps:** update multiplex libp2p-ipfs deps ([5605148](https://github.com/ipfs/js-ipfs/commit/5605148))
## [0.22.0](https://github.com/ipfs/js-ipfs/compare/v0.21.8...v0.22.0) (2017-02-15)
### Bug Fixes
* lint ([ffc120a](https://github.com/ipfs/js-ipfs/commit/ffc120a))
* make sure all deps are up to date, expose Buffer type ([7eb630d](https://github.com/ipfs/js-ipfs/commit/7eb630d))
* readable-stream needs to be 1.1.14 ([e999f05](https://github.com/ipfs/js-ipfs/commit/e999f05))
* tidy dag cli up ([b90ba76](https://github.com/ipfs/js-ipfs/commit/b90ba76))
### Features
* **breaking change:** experimental config options ([#749](https://github.com/ipfs/js-ipfs/issues/749)) ([69fa802](https://github.com/ipfs/js-ipfs/commit/69fa802))
* **dag:** basics (get, put) ([#746](https://github.com/ipfs/js-ipfs/issues/746)) ([e5ec0cf](https://github.com/ipfs/js-ipfs/commit/e5ec0cf))
* **dag:** Resolve API ([#751](https://github.com/ipfs/js-ipfs/issues/751)) ([4986908](https://github.com/ipfs/js-ipfs/commit/4986908))
* merge of get and resolve ([#761](https://github.com/ipfs/js-ipfs/issues/761)) ([b081e35](https://github.com/ipfs/js-ipfs/commit/b081e35))
## [0.21.8](https://github.com/ipfs/js-ipfs/compare/v0.21.7...v0.21.8) (2017-01-31)
### Features
* add CLI support for different hash func and type ([#748](https://github.com/ipfs/js-ipfs/issues/748)) ([a6c522f](https://github.com/ipfs/js-ipfs/commit/a6c522f))
## [0.21.7](https://github.com/ipfs/js-ipfs/compare/v0.21.6...v0.21.7) (2017-01-30)
### Bug Fixes
* default config file ([01ef4b5](https://github.com/ipfs/js-ipfs/commit/01ef4b5))
## [0.21.6](https://github.com/ipfs/js-ipfs/compare/v0.21.5...v0.21.6) (2017-01-29)
### Features
* bootstrap as an option ([#735](https://github.com/ipfs/js-ipfs/issues/735)) ([03362a3](https://github.com/ipfs/js-ipfs/commit/03362a3))
## [0.21.5](https://github.com/ipfs/js-ipfs/compare/v0.21.4...v0.21.5) (2017-01-29)
### Bug Fixes
* differenciate default config in browser and in node ([#734](https://github.com/ipfs/js-ipfs/issues/734)) ([17ccc8b](https://github.com/ipfs/js-ipfs/commit/17ccc8b))
## [0.21.4](https://github.com/ipfs/js-ipfs/compare/v0.21.3...v0.21.4) (2017-01-28)
### Bug Fixes
* ipfs.id does not double append ipfs/ anymore ([#732](https://github.com/ipfs/js-ipfs/issues/732)) ([718394a](https://github.com/ipfs/js-ipfs/commit/718394a))
## [0.21.3](https://github.com/ipfs/js-ipfs/compare/v0.21.2...v0.21.3) (2017-01-25)
## [0.21.2](https://github.com/ipfs/js-ipfs/compare/v0.21.1...v0.21.2) (2017-01-23)
## [0.21.1](https://github.com/ipfs/js-ipfs/compare/v0.21.0...v0.21.1) (2017-01-23)
## [0.21.0](https://github.com/ipfs/js-ipfs/compare/v0.20.4...v0.21.0) (2017-01-17)
### Bug Fixes
* point to a specific go-ipfs version (still waiting for another 0.4.5 pre release though ([19dbb1e](https://github.com/ipfs/js-ipfs/commit/19dbb1e))
## [0.20.4](https://github.com/ipfs/js-ipfs/compare/v0.20.2...v0.20.4) (2016-12-26)
### Bug Fixes
* bitswap wantlist http endpoint ([58f0885](https://github.com/ipfs/js-ipfs/commit/58f0885))
* bitswap wantlist stats ([9db86f5](https://github.com/ipfs/js-ipfs/commit/9db86f5))
* change default values of js-ipfs to avoid clash with go-ipfs + clean the browserify example ([6d52e1c](https://github.com/ipfs/js-ipfs/commit/6d52e1c))
* npm scripts ([eadcec0](https://github.com/ipfs/js-ipfs/commit/eadcec0))
* pass a first arg to bitswap to be removed after new bitswap is merged, so that tests pass now ([bddcee7](https://github.com/ipfs/js-ipfs/commit/bddcee7))
### Features
* **init:** add empty unixfs dir to match go-ipfs ([a967bb0](https://github.com/ipfs/js-ipfs/commit/a967bb0))
* **object:** add template option to object.new ([9058118](https://github.com/ipfs/js-ipfs/commit/9058118))
* add multicastdns to the mix ([c2ddefb](https://github.com/ipfs/js-ipfs/commit/c2ddefb))
## [0.20.2](https://github.com/ipfs/js-ipfs/compare/v0.20.1...v0.20.2) (2016-12-09)
### Bug Fixes
* **cli:** Tell user to init repo if not initialized when starting daemon ([fa7e275](https://github.com/ipfs/js-ipfs/commit/fa7e275))
## [0.20.1](https://github.com/ipfs/js-ipfs/compare/v0.19.0...v0.20.1) (2016-11-28)
## [0.19.0](https://github.com/ipfs/js-ipfs/compare/v0.18.0...v0.19.0) (2016-11-26)
### Bug Fixes
* addLink and rmLink ([7fad4d8](https://github.com/ipfs/js-ipfs/commit/7fad4d8))
* apply CR ([698f708](https://github.com/ipfs/js-ipfs/commit/698f708))
* **lint:** install missing plugin ([20e3d2e](https://github.com/ipfs/js-ipfs/commit/20e3d2e))
* **lint:** use eslint directly ([443dd9e](https://github.com/ipfs/js-ipfs/commit/443dd9e))
* **lint and polish:** add a little more comments ([d6ce83d](https://github.com/ipfs/js-ipfs/commit/d6ce83d))
### Features
* **cli:** migrate to awesome-dag-pb ([3bb3ba8](https://github.com/ipfs/js-ipfs/commit/3bb3ba8))
* **core:** migrate to awesome dag-pb ([db550a1](https://github.com/ipfs/js-ipfs/commit/db550a1))
* **examples:** add a getting-started example ([7485ac5](https://github.com/ipfs/js-ipfs/commit/7485ac5))
* **http:** migrate to awesome dag-pb ([ca9935f](https://github.com/ipfs/js-ipfs/commit/ca9935f))
* **swarm:** update swarm.peers to new api ([265a77a](https://github.com/ipfs/js-ipfs/commit/265a77a))
## [0.18.0](https://github.com/ipfs/js-ipfs/compare/v0.17.0...v0.18.0) (2016-11-12)
### Bug Fixes
* async .key ([2d2185b](https://github.com/ipfs/js-ipfs/commit/2d2185b))
* don't break backwards compatibility on the Block API ([3674b8e](https://github.com/ipfs/js-ipfs/commit/3674b8e))
* **cli:** alias add, cat and get to top-level cli ([6ad325b](https://github.com/ipfs/js-ipfs/commit/6ad325b))
### Features
* block API uses CIDs ([2eeea35](https://github.com/ipfs/js-ipfs/commit/2eeea35))
* migrate cli to use new async DAGNode interface ([1b0b22d](https://github.com/ipfs/js-ipfs/commit/1b0b22d))
* migrate core to use new async DAGNode interface ([254afdc](https://github.com/ipfs/js-ipfs/commit/254afdc))
* migrate files to use IPLD Resolver ([0fb1a1a](https://github.com/ipfs/js-ipfs/commit/0fb1a1a))
* migrate http-api to use new async DAGNode interface ([01e56ec](https://github.com/ipfs/js-ipfs/commit/01e56ec))
* migrate init to IPLD resolver ([61d1084](https://github.com/ipfs/js-ipfs/commit/61d1084))
* object API internals updated to use CID ([5cb10cc](https://github.com/ipfs/js-ipfs/commit/5cb10cc))
* update cli and http to support new ipld block api with IPLD ([5dbb799](https://github.com/ipfs/js-ipfs/commit/5dbb799))
* **http:** better error messages ([cd7f77d](https://github.com/ipfs/js-ipfs/commit/cd7f77d))
* **http:** set default headers for browsers ([6a21cd0](https://github.com/ipfs/js-ipfs/commit/6a21cd0))
## [0.17.0](https://github.com/ipfs/js-ipfs/compare/v0.16.0...v0.17.0) (2016-10-10)
### Bug Fixes
* **cli:** Fix issue with right cwd not being set ([e5f5e1b](https://github.com/ipfs/js-ipfs/commit/e5f5e1b))
* **deps:** move blob stores to dependencies ([8f33d11](https://github.com/ipfs/js-ipfs/commit/8f33d11))
* **files.get:** fix the command ([7015586](https://github.com/ipfs/js-ipfs/commit/7015586))
### Features
* **http-api:** add joi validation to bootstrap ([028a98c](https://github.com/ipfs/js-ipfs/commit/028a98c))
## [0.16.0](https://github.com/ipfs/js-ipfs/compare/v0.15.0...v0.16.0) (2016-09-15)
### Bug Fixes
* **cli:** add output for cli init ([29c9793](https://github.com/ipfs/js-ipfs/commit/29c9793))
* always use files.cat ([5b8da13](https://github.com/ipfs/js-ipfs/commit/5b8da13))
* **cli:** make ipfs files add work online and offline ([3edc2b9](https://github.com/ipfs/js-ipfs/commit/3edc2b9)), closes [#480](https://github.com/ipfs/js-ipfs/issues/480)
* **cli:** pipe content to the cli from cat it is a stream ([3e4e2fd](https://github.com/ipfs/js-ipfs/commit/3e4e2fd))
* **cli:** use right argument for cli .cat ([2bf49ea](https://github.com/ipfs/js-ipfs/commit/2bf49ea))
* **cli:** use right argument for cli .cat ([dd3fe88](https://github.com/ipfs/js-ipfs/commit/dd3fe88))
* **config:** better http-api and interface-ipfs-core compliant ([2beac9c](https://github.com/ipfs/js-ipfs/commit/2beac9c))
* **http:** get handler reads the stream ([b0a6db9](https://github.com/ipfs/js-ipfs/commit/b0a6db9))
* **swarm:** fix cli commands and enable tests ([6effa19](https://github.com/ipfs/js-ipfs/commit/6effa19))
* **version:** better http-api and interface-ipfs-core compliant ([0ee7215](https://github.com/ipfs/js-ipfs/commit/0ee7215))
### Features
* **add:** add the http endpoint for files.add ([e29f429](https://github.com/ipfs/js-ipfs/commit/e29f429))
* **files:** get interface-ipfs-core files tests pass through http-api ([11cb4ca](https://github.com/ipfs/js-ipfs/commit/11cb4ca))
* **files:** interface-ipfs-core tests over ipfs-api ([001a6eb](https://github.com/ipfs/js-ipfs/commit/001a6eb))
* **swarm:** interface-ipfs-core swarm compatibility ([3b32dfd](https://github.com/ipfs/js-ipfs/commit/3b32dfd))
* **swarm:** make interface-ipfs-core compliant ([ef729bb](https://github.com/ipfs/js-ipfs/commit/ef729bb)), closes [#439](https://github.com/ipfs/js-ipfs/issues/439)
* **tests:** waste less time generating keys ([cb10ab7](https://github.com/ipfs/js-ipfs/commit/cb10ab7))
## [0.15.0](https://github.com/ipfs/js-ipfs/compare/v0.14.3...v0.15.0) (2016-09-09)
### Bug Fixes
* **cli:** fix the files API commands ([138f519](https://github.com/ipfs/js-ipfs/commit/138f519))
* **config:** support null values (0 or empty string) on get and set ([a3d98a8](https://github.com/ipfs/js-ipfs/commit/a3d98a8))
* **repo:** init does not break if no opts are passed. Fixes [#349](https://github.com/ipfs/js-ipfs/issues/349) ([ca700cc](https://github.com/ipfs/js-ipfs/commit/ca700cc))
* **style:** apply CR ([97af048](https://github.com/ipfs/js-ipfs/commit/97af048))
* **test:** make the version test fetch the version from package.json instead of a hardcoded value ([50c9f7c](https://github.com/ipfs/js-ipfs/commit/50c9f7c))
### Features
* **bitswap tests, config, id:** cope with the nuances of the config API (.replace) and make necessary changes to make it all work again ([cc0c8fd](https://github.com/ipfs/js-ipfs/commit/cc0c8fd))
* **block-core:** add compliance with interface-ipfs-core on block-API ([5e6387d](https://github.com/ipfs/js-ipfs/commit/5e6387d))
* **block-http:** tests passing according with compliance ([a4071f0](https://github.com/ipfs/js-ipfs/commit/a4071f0))
* **config:** make the config impl spec compliant ([76b6670](https://github.com/ipfs/js-ipfs/commit/76b6670))
* **config-http:** return error if value is invalid ([f7a668d](https://github.com/ipfs/js-ipfs/commit/f7a668d))
* **factory:** add ipfs factory to files ([eba0398](https://github.com/ipfs/js-ipfs/commit/eba0398))
* **factory:** add ipfs factory, verify it works with object tests ([3db096e](https://github.com/ipfs/js-ipfs/commit/3db096e))
* **files.add:** update API to conform latest interface-ipfs-core updates ([28b0bb7](https://github.com/ipfs/js-ipfs/commit/28b0bb7))
* **http:** Refactor inject tests, made them all pass again ([31f673d](https://github.com/ipfs/js-ipfs/commit/31f673d))
* **http:** refactor ipfs-api tests and make them all pass again ([56904fd](https://github.com/ipfs/js-ipfs/commit/56904fd))
* **object-http:** support protobuf encoded values ([5f02303](https://github.com/ipfs/js-ipfs/commit/5f02303))
* **roadmap:** update ([418660f](https://github.com/ipfs/js-ipfs/commit/418660f))
* **roadmap:** update roadmap ms2 with extra added goals ([ac5352e](https://github.com/ipfs/js-ipfs/commit/ac5352e))
* disable PhantomJS ([921b11e](https://github.com/ipfs/js-ipfs/commit/921b11e))
* **tests:** all tests running ([44dba6c](https://github.com/ipfs/js-ipfs/commit/44dba6c))
* **tests:** factory-http ([08a4b19](https://github.com/ipfs/js-ipfs/commit/08a4b19))
## [0.14.3](https://github.com/ipfs/js-ipfs/compare/v0.14.2...v0.14.3) (2016-08-10)
### Features
* **interface:** update interface-ipfs-core to v0.6.0 ([d855740](https://github.com/ipfs/js-ipfs/commit/d855740))
## [0.14.2](https://github.com/ipfs/js-ipfs/compare/v0.14.1...v0.14.2) (2016-08-09)
### Bug Fixes
* upgrade aegir and ensure glob is mocked ([3c70eaa](https://github.com/ipfs/js-ipfs/commit/3c70eaa)), closes [#354](https://github.com/ipfs/js-ipfs/issues/354) [#353](https://github.com/ipfs/js-ipfs/issues/353)
* **cli:** replace ronin with yargs ([cba42ca](https://github.com/ipfs/js-ipfs/commit/cba42ca)), closes [#331](https://github.com/ipfs/js-ipfs/issues/331)
* **version:** return actual js-ipfs version ([6377ab2](https://github.com/ipfs/js-ipfs/commit/6377ab2)), closes [#377](https://github.com/ipfs/js-ipfs/issues/377)
* use static version of package.json ([3ffdc27](https://github.com/ipfs/js-ipfs/commit/3ffdc27))
### Features
* update all dependencies ([b90747e](https://github.com/ipfs/js-ipfs/commit/b90747e))
## [0.14.1](https://github.com/ipfs/js-ipfs/compare/v0.14.0...v0.14.1) (2016-06-29)
## [0.14.0](https://github.com/ipfs/js-ipfs/compare/v0.13.0...v0.14.0) (2016-06-27)
## [0.13.0](https://github.com/ipfs/js-ipfs/compare/v0.12.0...v0.13.0) (2016-06-07)
## [0.12.0](https://github.com/ipfs/js-ipfs/compare/v0.11.1...v0.12.0) (2016-06-06)
### Bug Fixes
* handle new wantlist format ([7850dbb](https://github.com/ipfs/js-ipfs/commit/7850dbb))
## [0.11.1](https://github.com/ipfs/js-ipfs/compare/v0.11.0...v0.11.1) (2016-05-30)
## [0.11.0](https://github.com/ipfs/js-ipfs/compare/v0.10.3...v0.11.0) (2016-05-27)
## [0.10.3](https://github.com/ipfs/js-ipfs/compare/v0.10.2...v0.10.3) (2016-05-26)
## [0.10.2](https://github.com/ipfs/js-ipfs/compare/v0.10.1...v0.10.2) (2016-05-26)
### Bug Fixes
* use passed in repo location in the browser ([4b55102](https://github.com/ipfs/js-ipfs/commit/4b55102))
## [0.10.1](https://github.com/ipfs/js-ipfs/compare/v0.10.0...v0.10.1) (2016-05-25)
## [0.10.0](https://github.com/ipfs/js-ipfs/compare/v0.9.0...v0.10.0) (2016-05-24)
## [0.9.0](https://github.com/ipfs/js-ipfs/compare/v0.8.0...v0.9.0) (2016-05-24)
## [0.8.0](https://github.com/ipfs/js-ipfs/compare/v0.7.0...v0.8.0) (2016-05-23)
## [0.7.0](https://github.com/ipfs/js-ipfs/compare/v0.6.1...v0.7.0) (2016-05-21)
### [0.6.1](https://github.com/ipfs/js-ipfs/compare/v0.6.0...v0.6.1) (2016-05-19)
## [0.6.0](https://github.com/ipfs/js-ipfs/compare/v0.5.0...v0.6.0) (2016-05-19)
## [0.5.0](https://github.com/ipfs/js-ipfs/compare/v0.4.10...v0.5.0) (2016-05-16)
### Bug Fixes
* **files:add:** simplify checkPath ([46d9e6a](https://github.com/ipfs/js-ipfs/commit/46d9e6a))
* **files:get:** simplify checkArgs ([7f89bfb](https://github.com/ipfs/js-ipfs/commit/7f89bfb))
* **http:object:** proper handling of empty args ([9763f86](https://github.com/ipfs/js-ipfs/commit/9763f86))
### Features
* integrate libp2p-ipfs-browser ([6022b46](https://github.com/ipfs/js-ipfs/commit/6022b46))
* make core/object satisfy interface-ipfs-core ([96013bb](https://github.com/ipfs/js-ipfs/commit/96013bb))
### [0.4.10](https://github.com/ipfs/js-ipfs/compare/v0.4.9...v0.4.10) (2016-05-08)
### Bug Fixes
* **cli:** self host cmds listing ([a415dc1](https://github.com/ipfs/js-ipfs/commit/a415dc1))
* **core:** consistent repo.exists checks ([3d1e6b0](https://github.com/ipfs/js-ipfs/commit/3d1e6b0))
### [0.4.9](https://github.com/ipfs/js-ipfs/compare/v0.4.8...v0.4.9) (2016-04-28)
### [0.4.8](https://github.com/ipfs/js-ipfs/compare/v0.4.7...v0.4.8) (2016-04-28)
### [0.4.7](https://github.com/ipfs/js-ipfs/compare/v0.4.6...v0.4.7) (2016-04-25)
### [0.4.6](https://github.com/ipfs/js-ipfs/compare/v0.4.4...v0.4.6) (2016-04-22)
### [0.4.4](https://github.com/ipfs/js-ipfs/compare/v0.4.3...v0.4.4) (2016-03-22)
### [0.4.3](https://github.com/ipfs/js-ipfs/compare/v0.4.2...v0.4.3) (2016-03-21)
### [0.4.2](https://github.com/ipfs/js-ipfs/compare/v0.4.1...v0.4.2) (2016-03-21)
### [0.4.1](https://github.com/ipfs/js-ipfs/compare/v0.4.0...v0.4.1) (2016-03-16)
## [0.4.0](https://github.com/ipfs/js-ipfs/compare/v0.3.1...v0.4.0) (2016-02-23)
### [0.3.1](https://github.com/ipfs/js-ipfs/compare/v0.3.0...v0.3.1) (2016-02-19)
## [0.3.0](https://github.com/ipfs/js-ipfs/compare/v0.2.3...v0.3.0) (2016-02-03)
### [0.2.3](https://github.com/ipfs/js-ipfs/compare/v0.2.2...v0.2.3) (2016-01-31)
### [0.2.2](https://github.com/ipfs/js-ipfs/compare/v0.2.1...v0.2.2) (2016-01-28)
### [0.2.1](https://github.com/ipfs/js-ipfs/compare/v0.2.0...v0.2.1) (2016-01-28)
## [0.2.0](https://github.com/ipfs/js-ipfs/compare/v0.0.3...v0.2.0) (2016-01-27)
### [0.0.3](https://github.com/ipfs/js-ipfs/compare/v0.0.2...v0.0.3) (2016-01-15)
## 0.0.2 (2016-01-11)
================================================
FILE: packages/ipfs/CODE_OF_CONDUCT.md
================================================
# Contributor Code of Conduct
The `js-ipfs` project follows the [`IPFS Community Code of Conduct`](https://github.com/ipfs/community/blob/master/code-of-conduct.md)
================================================
FILE: packages/ipfs/CONTRIBUTING.md
================================================
# Contributing guidelines
IPFS as a project, including js-ipfs and all of its modules, follows the [standard IPFS Community contributing guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md).
We also adhere to the [IPFS JavaScript Community contributing guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) which provide additional information of how to collaborate and contribute in the JavaScript implementation of IPFS.
We appreciate your time and attention for going over these. Please open an issue on [ipfs/community](https://github.com/ipfs/community) if you have any question.
Thank you.
================================================
FILE: packages/ipfs/COPYRIGHT
================================================
This project is transitioning from an MIT-only license to a dual MIT/Apache-2.0 license.
Unless otherwise noted, all code contributed prior to 2019-11-21 and not contributed by
a user listed in [this signoff issue](https://github.com/ipfs/js-ipfs/issues/2624) is
licensed under MIT-only. All new contributions (and past contributions since 2019-11-21)
are licensed under a dual MIT/Apache-2.0 license.
================================================
FILE: packages/ipfs/LICENSE
================================================
This project is dual licensed under MIT and Apache-2.0.
MIT: https://www.opensource.org/licenses/mit
Apache-2.0: https://www.apache.org/licenses/license-2.0
================================================
FILE: packages/ipfs/LICENSE-APACHE
================================================
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
================================================
FILE: packages/ipfs/LICENSE-MIT
================================================
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: packages/ipfs/Makefile
================================================
all: help
test: test_expensive
test_short: test_sharness_short
test_expensive: test_sharness_expensive
test_sharness_short:
$(MAKE) -j1 -C test/sharness/
test_sharness_expensive:
TEST_EXPENSIVE=1 $(MAKE) -j1 -C test/sharness/
help:
@echo 'TESTING TARGETS:'
@echo ''
@echo ' test - Run expensive tests'
@echo ' test_short - Run short tests and sharness tests'
@echo ' test_expensive - Run a few extras'
@echo ' test_sharness_short'
@echo ' test_sharness_expensive'
@echo ''
================================================
FILE: packages/ipfs/README.md
================================================
> # ⛔️ DEPRECATED: [js-IPFS](https://github.com/ipfs/js-ipfs) has been superseded by [Helia](https://github.com/ipfs/helia)
>
> 📚 [Learn more about this deprecation](https://github.com/ipfs/js-ipfs/issues/4336) or [how to migrate](https://github.com/ipfs/helia/wiki/Migrating-from-js-IPFS)
>
> ⚠️ If you continue using this repo, please note that security fixes will not be provided
# ipfs
[](https://ipfs.tech)
[](https://discuss.ipfs.tech)
[](https://codecov.io/gh/ipfs/js-ipfs)
[](https://github.com/ipfs/js-ipfs/actions/workflows/test.yml?query=branch%3Amaster)
> JavaScript implementation of the IPFS specification
## Table of contents
- [Install](#install)
- [Getting Started](#getting-started)
- [Next Steps](#next-steps)
- [Want to hack on IPFS?](#want-to-hack-on-ipfs)
- [License](#license)
- [Contribute](#contribute)
## Install
```console
$ npm i ipfs
```
`ipfs` is the core API, a CLI and a HTTP server that functions as a HTTP to IPFS bridge and an RPC endpoint.
If you want to integrate IPFS into your application without including a CLI or HTTP server, see the [ipfs-core](https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs-core) module.
## Getting Started
Installing `ipfs` globally will give you the `jsipfs` command which you can use to start a daemon running:
```console
$ npm install -g ipfs
$ jsipfs daemon
Initializing IPFS daemon...
js-ipfs version: x.x.x
System version: x64/darwin
Node.js version: x.x.x
Swarm listening on /ip4/127.0
.... more output
```
You can then add a file:
```console
$ jsipfs add ./hello-world.txt
added QmXXY5ZxbtuYj6DnfApLiGstzPN7fvSyigrRee3hDWPCaf hello-world.txt
```
### Next Steps
- Read the [docs](https://github.com/ipfs/js-ipfs/tree/master/docs)
- Look into the [examples](https://github.com/ipfs-examples/js-ipfs-examples) to learn how to spawn an IPFS node in Node.js and in the Browser
- Consult the [Core API docs](https://github.com/ipfs/js-ipfs/tree/master/docs/core-api) to see what you can do with an IPFS node
- Head over to to take interactive tutorials that cover core IPFS APIs
- Check out for tips, how-tos and more
- See for news and more
- Need help? Please ask 'How do I?' questions on
## Want to hack on IPFS?
[](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md)
This IPFS implementation in JavaScript needs your help! There are a few things you can do right now to help out:
Read the [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md) and [JavaScript Contributing Guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md).
- **Check out existing issues** The [issue list](https://github.com/ipfs/js-ipfs/issues) has many that are marked as ['help wanted'](https://github.com/ipfs/js-ipfs/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22help+wanted%22) or ['difficulty:easy'](https://github.com/ipfs/js-ipfs/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Adifficulty%3Aeasy) which make great starting points for development, many of which can be tackled with no prior IPFS knowledge
- **Perform code reviews** More eyes will help
a. speed the project along
b. ensure quality, and
c. reduce possible future bugs.
- **Add tests**. There can never be enough tests.
Find out about chat channels, the IPFS newsletter, the IPFS blog, and more in the [IPFS community space](https://docs.ipfs.io/community/).
## License
Licensed under either of
- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / )
- MIT ([LICENSE-MIT](LICENSE-MIT) / )
## Contribute
Contributions welcome! Please check out [the issues](https://github.com/ipfs/js-ipfs/issues).
Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general.
Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md).
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
[](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md)
================================================
FILE: packages/ipfs/init-and-daemon.sh
================================================
#! /usr/bin/env bash
set -e
if [ -n "$IPFS_PATH" ]; then
echo "Using $IPFS_PATH as IPFS repository"
else
echo "You need to set IPFS_PATH environment variable to use this script"
exit 1
fi
# Initialize the repo but ignore if error if it already exists
# This can be the case when we restart a container without stopping/removing it
node src/cli.js init || true
if [ -n "$IPFS_API_HOST" ]; then
sed -i.bak "s/127.0.0.1/$IPFS_API_HOST/g" $IPFS_PATH/config
fi
node src/cli.js daemon
================================================
FILE: packages/ipfs/maintainer.json
================================================
{
"repoLeadMaintainer": {
"name": "Alan Shaw",
"email": "alan.shaw@protocol.ai",
"username": "alanshaw"
},
"workingGroup": {
"name": "JS IPFS",
"entryPoint": "https://github.com/ipfs/js-core"
}
}
================================================
FILE: packages/ipfs/package.json
================================================
{
"name": "ipfs",
"version": "0.66.1",
"description": "JavaScript implementation of the IPFS specification",
"license": "Apache-2.0 OR MIT",
"homepage": "https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/ipfs/js-ipfs.git"
},
"bugs": {
"url": "https://github.com/ipfs/js-ipfs/issues"
},
"keywords": [
"IPFS"
],
"engines": {
"node": ">=16.0.0",
"npm": ">=7.0.0"
},
"bin": {
"jsipfs": "src/cli.js"
},
"type": "module",
"types": "./dist/src/index.d.ts",
"typesVersions": {
"*": {
"*": [
"*",
"dist/*",
"dist/src/*",
"dist/src/*/index"
],
"src/*": [
"*",
"dist/*",
"dist/src/*",
"dist/src/*/index"
]
}
},
"files": [
"src",
"dist",
"!dist/test",
"!**/*.tsbuildinfo"
],
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./src/index.js"
},
"./path": {
"types": "./src/path.d.ts",
"browser": "./src/path.browser.js",
"import": "./src/path.js"
}
},
"eslintConfig": {
"extends": "ipfs",
"parserOptions": {
"sourceType": "module"
}
},
"scripts": {
"build": "aegir build",
"prepublishOnly": "node scripts/update-version.js",
"lint": "aegir lint",
"test:interface:core": "aegir test -f test/interface-core.js",
"test:interface:client": "aegir test -f test/interface-client.js",
"test:interface:http-js": "aegir test -f test/interface-http-js.js",
"test:interface:http-go": "aegir test -f test/interface-http-go.js",
"test:interop": "cross-env DEBUG=$DEBUG IPFS_LOGGING=$IPFS_LOGGING IPFS_JS_EXEC=$PWD/src/cli.js KUBO_RPC_MODULE=$PWD/../ipfs-http-client/src/index.js LIBP2P_TCP_REUSEPORT=false ipfs-interop",
"test:external": "aegir test-dependant",
"clean": "aegir clean",
"dep-check": "aegir dep-check -i ipfs-core-types -i @types/*"
},
"dependencies": {
"@libp2p/logger": "^2.0.5",
"ipfs-cli": "^0.16.1",
"ipfs-core": "^0.18.1",
"semver": "^7.3.2",
"update-notifier": "^6.0.0"
},
"devDependencies": {
"@libp2p/webrtc-star-signalling-server": "^3.0.0",
"@libp2p/websockets": "^5.0.0",
"@types/semver": "^7.3.4",
"@types/update-notifier": "^6.0.1",
"aegir": "^37.11.0",
"cross-env": "^7.0.0",
"go-ipfs": "^0.12.0",
"interface-ipfs-core": "^0.158.1",
"ipfs-client": "^0.10.1",
"ipfs-core-types": "^0.14.1",
"ipfs-http-client": "^60.0.1",
"ipfs-interop": "^10.0.0",
"ipfs-utils": "^9.0.13",
"ipfsd-ctl": "^13.0.0",
"iso-url": "^1.0.0",
"kubo-rpc-client": "^3.0.0",
"merge-options": "^3.0.4",
"mock-ipfs-pinning-service": "^0.4.2",
"url": "^0.11.0"
},
"optionalDependencies": {
"electron-webrtc": "^0.3.0",
"wrtc": "^0.4.6"
},
"browser": {
"./src/cli.js": false,
"./src/path.js": "./src/path.browser.js",
"go-ipfs": false
}
}
================================================
FILE: packages/ipfs/scripts/update-version.js
================================================
import { readFile, writeFile } from 'fs/promises'
const pkg = JSON.parse(
await readFile(
new URL('../package.json', import.meta.url)
)
)
await writeFile(
new URL('../src/package.js', import.meta.url),
`
export const name = '${pkg.name}'
export const version = '${pkg.version}'
export const node = '${pkg.engines.node}'
`
)
================================================
FILE: packages/ipfs/src/cli.js
================================================
#! /usr/bin/env node
/* eslint-disable no-console */
/**
* Handle any uncaught errors
*
* @param {any} err
* @param {string} [origin]
*/
import semver from 'semver'
import * as pkg from './package.js'
import { logger } from '@libp2p/logger'
import { print, getIpfs, getRepoPath } from 'ipfs-cli/utils'
import { cli } from 'ipfs-cli'
import updateNotifier from 'update-notifier'
/**
* @param {any} err
* @param {string} origin
*/
const onUncaughtException = (err, origin) => {
if (!origin || origin === 'uncaughtException') {
console.error(err)
process.exit(1)
}
}
/**
* Handle any uncaught errors
*
* @param {any} err
*/
const onUnhandledRejection = (err) => {
console.error(err)
process.exit(1)
}
process.once('uncaughtException', onUncaughtException)
process.once('unhandledRejection', onUnhandledRejection)
if (process.env.DEBUG) {
process.on('warning', err => {
console.error(err.stack)
})
}
const log = logger('ipfs:cli')
process.title = pkg.name
// Check for node version
if (!semver.satisfies(process.versions.node, pkg.node)) {
console.error(`Please update your Node.js version to ${pkg.node}`)
process.exit(1)
}
// If we're not running an rc, check if an update is available and notify
if (!pkg.version.includes('-rc')) {
const oneWeek = 1000 * 60 * 60 * 24 * 7
updateNotifier({ pkg, updateCheckInterval: oneWeek }).notify()
}
/**
* @param {string[]} argv
*/
async function main (argv) {
let exitCode = 0
let ctx = {
print,
getStdin: () => process.stdin,
repoPath: getRepoPath(),
cleanup: () => {},
isDaemon: false,
/** @type {import('ipfs-core-types').IPFS | undefined} */
ipfs: undefined
}
const command = argv.slice(2)
try {
await cli(command, async (argv) => {
if (!['daemon', 'init'].includes(command[0])) {
// @ts-expect-error argv as no properties in common
const { ipfs, isDaemon, cleanup } = await getIpfs(argv)
ctx = {
...ctx,
ipfs,
isDaemon,
cleanup
}
}
argv.ctx = ctx
})
} catch (/** @type {any} */ err) {
// TODO: export errors from ipfs-repo to use .code constants
if (err.code === 'ERR_INVALID_REPO_VERSION') {
err.message = 'Incompatible repo version. Migration needed. Pass --migrate for automatic migration'
}
if (err.code === 'ERR_NOT_ENABLED') {
err.message = `no IPFS repo found in ${ctx.repoPath}.\nplease run: 'ipfs init'`
}
// Handle yargs errors
if (err.code === 'ERR_YARGS') {
err.yargs.showHelp()
ctx.print.error('\n')
ctx.print.error(`Error: ${err.message}`)
} else if (log.enabled) {
// Handle commands handler errors
log(err)
} else {
ctx.print.error(err.message)
}
exitCode = 1
} finally {
await ctx.cleanup()
}
if (command[0] === 'daemon') {
// don't shut down the daemon process
return
}
process.exit(exitCode)
}
main(process.argv)
================================================
FILE: packages/ipfs/src/index.js
================================================
import {
create as createImport,
globSource as globSourceImport,
urlSource as urlSourceImport
} from 'ipfs-core'
import {
path as pathImport
} from './path.js'
/**
* @typedef {import('ipfs-core-types').IPFS} IPFS
*/
export const create = createImport
export const globSource = globSourceImport
export const urlSource = urlSourceImport
export const path = pathImport
================================================
FILE: packages/ipfs/src/package.js
================================================
export const name = 'ipfs'
export const version = '0.62.2'
export const node = '>=15.0.0'
================================================
FILE: packages/ipfs/src/path.browser.js
================================================
export function path () {
throw new Error('Not supported in browsers')
}
================================================
FILE: packages/ipfs/src/path.js
================================================
import fs from 'fs'
import Path from 'path'
export function path () {
const paths = []
// simulate node's node_modules lookup
for (let i = 0; i < process.cwd().split(Path.sep).length; i++) {
const dots = new Array(i).fill('..')
paths.push(
Path.resolve(
Path.join(process.cwd(), ...dots, 'node_modules', 'ipfs')
)
)
}
const resourcePath = paths.find(path => fs.existsSync(path))
if (!resourcePath) {
throw new Error(`Could not find ipfs module in paths: \n${paths.join('\n')}`)
}
const pkg = JSON.parse(fs.readFileSync(resourcePath + Path.sep + 'package.json', {
encoding: 'utf-8'
}))
const bin = pkg.bin.jsipfs
return Path.resolve(Path.join(resourcePath, bin))
}
================================================
FILE: packages/ipfs/test/interface-client.js
================================================
/* eslint-env mocha, browser */
import * as tests from 'interface-ipfs-core'
import { isNode } from 'ipfs-utils/src/env.js'
import { factory } from './utils/factory.js'
import * as ipfsClientModule from 'ipfs-client'
describe('interface-ipfs-core ipfs-client tests', () => {
const commonFactory = factory({
type: 'js',
ipfsClientModule
})
tests.files(commonFactory, {
skip: [{
name: '.files.chmod',
reason: 'not implemented'
}, {
name: '.files.cp',
reason: 'not implemented'
}, {
name: '.files.mkdir',
reason: 'not implemented'
}, {
name: '.files.stat',
reason: 'not implemented'
}, {
name: '.files.touch',
reason: 'not implemented'
}, {
name: '.files.rm',
reason: 'not implemented'
}, {
name: '.files.read',
reason: 'not implemented'
}, {
name: '.files.mv',
reason: 'not implemented'
}, {
name: '.files.flush',
reason: 'not implemented'
}].concat(isNode
? []
: [{
name: 'should make directory and specify mtime as hrtime',
reason: 'Not designed to run in the browser'
}, {
name: 'should set mtime as hrtime',
reason: 'Not designed to run in the browser'
}, {
name: 'should write file and specify mtime as hrtime',
reason: 'Not designed to run in the browser'
}])
})
tests.root(commonFactory, {
skip: [
{
name: 'add',
reason: 'not implemented'
},
{
name: 'should add with only-hash=true',
reason: 'ipfs.object.get is not implemented'
},
{
name: 'should add a directory with only-hash=true',
reason: 'ipfs.object.get is not implemented'
},
{
name: 'should add with mtime as hrtime',
reason: 'process.hrtime is not a function in browser'
},
{
name: 'should add from a URL with only-hash=true',
reason: 'ipfs.object.get is not implemented'
},
{
name: 'should cat with a Uint8Array multihash',
reason: 'Passing CID as Uint8Array is not supported'
},
{
name: 'should add from a HTTP URL',
reason: 'https://github.com/ipfs/js-ipfs/issues/3195'
},
{
name: 'should add from a HTTP URL with redirection',
reason: 'https://github.com/ipfs/js-ipfs/issues/3195'
},
{
name: 'should add from a URL with only-hash=true',
reason: 'https://github.com/ipfs/js-ipfs/issues/3195'
},
{
name: 'should add from a URL with wrap-with-directory=true',
reason: 'https://github.com/ipfs/js-ipfs/issues/3195'
},
{
name: 'should add from a URL with wrap-with-directory=true and URL-escaped file name',
reason: 'https://github.com/ipfs/js-ipfs/issues/3195'
},
{
name: 'should not add from an invalid url',
reason: 'https://github.com/ipfs/js-ipfs/issues/3195'
},
{
name: 'should be able to add dir without sharding',
reason: 'Cannot spawn IPFS with different args'
},
{
name: 'with sharding',
reason: 'TODO: allow spawning new daemons with different config'
},
{
name: 'get',
reason: 'Not implemented'
},
{
name: 'refs',
reason: 'Not implemented'
},
{
name: 'refsLocal',
reason: 'Not implemented'
}
]
})
tests.miscellaneous(commonFactory, {
skip: [
{
name: '.dns',
reason: 'Not implemented'
},
{
name: '.resolve',
reason: 'Not implemented'
},
{
name: '.stop',
reason: 'Not implemented'
},
{
name: '.version',
reason: 'Not implemented'
}
]
})
tests.pubsub(factory({
type: 'js',
ipfsClientModule
}))
})
================================================
FILE: packages/ipfs/test/interface-core.js
================================================
/* eslint-env mocha, browser */
import * as tests from 'interface-ipfs-core'
import { isNode } from 'ipfs-utils/src/env.js'
import { factory } from './utils/factory.js'
import * as ipfsClientModule from 'ipfs-client'
/** @typedef { import("ipfsd-ctl").ControllerOptions } ControllerOptions */
describe('interface-ipfs-core tests', function () {
const commonFactory = factory({
ipfsClientModule
})
tests.root(commonFactory, {
skip: isNode
? []
: [{
name: 'should add with mtime as hrtime',
reason: 'Not designed to run in the browser'
}]
})
tests.bitswap(commonFactory)
tests.block(commonFactory)
tests.bootstrap(commonFactory)
tests.config(commonFactory)
tests.dag(commonFactory)
tests.dht(commonFactory)
tests.files(factory(), {
skip: isNode
? null
: [{
name: 'should make directory and specify mtime as hrtime',
reason: 'Not designed to run in the browser'
}, {
name: 'should set mtime as hrtime',
reason: 'Not designed to run in the browser'
}, {
name: 'should write file and specify mtime as hrtime',
reason: 'Not designed to run in the browser'
}]
})
tests.key(commonFactory)
tests.miscellaneous(commonFactory, {
skip: [
{
name: 'should include the ipfs-http-client version',
reason: 'Value is added by the HTTP RPC API server which is not part of ipfs-core'
}
]
})
tests.name(factory({
ipfsOptions: {
offline: true
}
}))
tests.namePubsub(factory({
ipfsOptions: {
EXPERIMENTAL: {
ipnsPubsub: true
}
}
}))
tests.object(commonFactory)
tests.pin(commonFactory, {
skip: [{
name: '.pin.remote.service',
reason: 'Not implemented'
}, {
name: '.pin.remote.add',
reason: 'Not implemented'
}, {
name: '.pin.remote.ls',
reason: 'Not implemented'
}, {
name: '.pin.remote.rm',
reason: 'Not implemented'
}, {
name: '.pin.remote.rmAll',
reason: 'Not implemented'
}]
})
tests.ping(commonFactory)
tests.pubsub(commonFactory, {
skip: [
...(isNode
? []
: [
{
name: 'should receive messages from a different node',
reason: 'https://github.com/ipfs/js-ipfs/issues/2662'
},
{
name: 'should round trip a non-utf8 binary buffer',
reason: 'https://github.com/ipfs/js-ipfs/issues/2662'
},
{
name: 'should receive multiple messages',
reason: 'https://github.com/ipfs/js-ipfs/issues/2662'
},
{
name: 'should send/receive 100 messages',
reason: 'https://github.com/ipfs/js-ipfs/issues/2662'
}])
]
})
tests.repo(commonFactory)
tests.stats(commonFactory)
tests.swarm(commonFactory)
})
================================================
FILE: packages/ipfs/test/interface-http-go.js
================================================
/* eslint-env mocha */
import * as tests from 'interface-ipfs-core'
import { factory } from './utils/factory.js'
const isWindows = globalThis.process && globalThis.process.platform && globalThis.process.platform === 'win32'
const isFirefox = globalThis.navigator?.userAgent?.toLowerCase().includes('firefox')
/** @typedef {import("ipfsd-ctl").ControllerOptions} ControllerOptions */
describe('interface-ipfs-core over ipfs-http-client tests against go-ipfs', () => {
const commonFactory = factory({
type: 'go'
})
tests.root(commonFactory, {
skip: [
{
name: 'should add with mode as string',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should add with mode as number',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should add with mtime as Date',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should add with mtime as { nsecs, secs }',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should add with mtime as timespec',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should add with mtime as hrtime',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should export a chunk of a file',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should ls with metadata',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should ls single file with metadata',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should ls single file without containing directory with metadata',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should override raw leaves when file is smaller than one block and metadata is present',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should add directories with metadata',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should support bidirectional streaming',
reason: 'Not supported by http'
},
{
name: 'should error during add-all stream',
reason: 'Not supported by http'
}
].concat(isFirefox
? [{
name: 'should add a BIG Uint8Array',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'should add a BIG Uint8Array with progress enabled',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'should add big files',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}]
: [])
})
tests.bitswap(commonFactory, {
skip: [
{
name: '.bitswap.unwant',
reason: 'TODO not implemented in go-ipfs yet'
}
]
})
tests.block(commonFactory)
tests.bootstrap(commonFactory)
tests.config(commonFactory, {
skip: [
// config.replace
{
name: 'replace',
reason: 'FIXME Waiting for fix on go-ipfs https://github.com/ipfs/js-ipfs-http-client/pull/307#discussion_r69281789 and https://github.com/ipfs/go-ipfs/issues/2927'
},
{
name: 'should list config profiles',
reason: 'TODO: Not implemented in go-ipfs'
},
{
name: 'should strip private key from diff output',
reason: 'TODO: Not implemented in go-ipfs'
}
]
})
tests.dag(commonFactory, {
skip: [
// dag.get:
{
name: 'should get only a CID, due to resolving locally only',
reason: 'FIXME: go-ipfs does not support localResolve option'
},
{
name: 'should get a node added as CIDv0 with a CIDv1',
reason: 'go-ipfs doesn\'t use CIDv0 for DAG API anymore'
}
]
})
tests.dht(commonFactory, {
skip: [
{
name: 'should error when DHT not available',
reason: 'go returns a query error'
}
]
})
tests.files(commonFactory, {
skip: [
{
name: 'should ls directory',
reason: 'TODO unskip when go-ipfs supports --long https://github.com/ipfs/go-ipfs/pull/6528'
},
{
name: 'should list a file directly',
reason: 'TODO unskip when go-ipfs supports --long https://github.com/ipfs/go-ipfs/pull/6528'
},
{
name: 'should ls directory and include metadata',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should read from outside of mfs',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should ls from outside of mfs',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should update the mode for a file',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should update the mode for a directory',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should update the mode for a hamt-sharded-directory',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should update modes with basic symbolic notation that adds bits',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should update modes with basic symbolic notation that removes bits',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should update modes with basic symbolic notation that overrides bits',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should update modes with multiple symbolic notation',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should update modes with special symbolic notation',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should apply special execute permissions to world',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should apply special execute permissions to user',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should apply special execute permissions to user and group',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should apply special execute permissions to sharded directories',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should update file mtime',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should update directory mtime',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should update the mtime for a hamt-sharded-directory',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should create an empty file',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should make directory and specify mode',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should make directory and specify mtime',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should write file and specify mode',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should write file and specify mtime',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should respect metadata when copying files',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should respect metadata when copying directories',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should respect metadata when copying from outside of mfs',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should have default mtime',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should set mtime as Date',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should set mtime as { nsecs, secs }',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should set mtime as timespec',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should set mtime as hrtime',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should make directory and have default mode',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should make directory and specify mode as string',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should make directory and specify mode as number',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should make directory and specify mtime as Date',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should make directory and specify mtime as { nsecs, secs }',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should make directory and specify mtime as timespec',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should make directory and specify mtime as hrtime',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should write file and specify mode as a string',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should write file and specify mode as a number',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should write file and specify mtime as Date',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should write file and specify mtime as { nsecs, secs }',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should write file and specify mtime as timespec',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should write file and specify mtime as hrtime',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should stat file with mode',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should stat file with mtime',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should stat dir with mode',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should stat dir with mtime',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should stat sharded dir with mode',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should stat sharded dir with mtime',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'lists a raw node',
reason: 'TODO go-ipfs does not support ipfs paths for all mfs commands'
},
{
name: 'lists a raw node in an mfs directory',
reason: 'TODO go-ipfs does not support non-ipfs nodes in mfs'
},
{
name: 'writes a small file with an escaped slash in the title',
reason: 'TODO go-ipfs does not support escapes in paths'
},
{
name: 'overwrites a file with a different CID version',
reason: 'TODO go-ipfs does not support changing the CID version'
},
{
name: 'partially overwrites a file with a different CID version',
reason: 'TODO go-ipfs does not support changing the CID version'
},
{
name: 'refuses to copy multiple files to a non-existent child directory',
reason: 'TODO go-ipfs does not support copying multiple files at once'
},
{
name: 'refuses to copy files to an unreadable node',
reason: 'TODO go-ipfs does not support identity format, maybe in 0.5.0?'
},
{
name: 'copies a file to a pre-existing directory',
reason: 'TODO go-ipfs does not copying files into existing directories if the directory is specify as the target path'
},
{
name: 'copies multiple files to new location',
reason: 'TODO go-ipfs does not support copying multiple files at once'
},
{
name: 'copies files to deep mfs paths and creates intermediate directories',
reason: 'TODO go-ipfs does not support the parents flag in the cp command'
},
{
name: 'copies a sharded directory to a normal directory',
reason: 'TODO go-ipfs does not copying files into existing directories if the directory is specify as the target path'
},
{
name: 'copies a normal directory to a sharded directory',
reason: 'TODO go-ipfs does not copying files into existing directories if the directory is specify as the target path'
},
{
name: 'removes multiple files',
reason: 'TODO go-ipfs does not support removing multiple files'
},
{
name: 'results in the same hash as a sharded directory created by the importer when removing a file',
reason: 'TODO go-ipfs errors out with HTTPError: Could not convert value "85675" to type "bool" (for option "-size")'
},
{
name: 'results in the same hash as a sharded directory created by the importer when removing a subshard',
reason: 'TODO go-ipfs errors out with HTTPError: Could not convert value "2109" to type "bool" (for option "-size")'
},
{
name: 'results in the same hash as a sharded directory created by the importer when removing a file from a subshard of a subshard',
reason: 'TODO go-ipfs errors out with HTTPError: Could not convert value "170441" to type "bool" (for option "-size")'
},
{
name: 'results in the same hash as a sharded directory created by the importer when removing a subshard of a subshard',
reason: 'TODO go-ipfs errors out with HTTPError: Could not convert value "11463" to type "bool" (for option "-size")'
},
{
name: 'results in the same hash as a sharded directory created by the importer when adding a new file',
reason: 'TODO go-ipfs errors out with HTTPError: Could not convert value "5835" to type "bool" (for option "-size")'
},
{
name: 'results in the same hash as a sharded directory created by the importer when creating a new subshard',
reason: 'TODO go-ipfs errors out with HTTPError: Could not convert value "8038" to type "bool" (for option "-size")'
},
{
name: ' results in the same hash as a sharded directory created by the importer when adding a file to a subshard',
reason: 'TODO go-ipfs errors out with HTTPError: Could not convert value "6620" to type "bool" (for option "-size")'
},
{
name: 'results in the same hash as a sharded directory created by the importer when adding a file to a subshard',
reason: 'HTTPError: Could not convert value "6620" to type "bool" (for option "-size")'
},
{
name: 'results in the same hash as a sharded directory created by the importer when adding a file to a subshard of a subshard',
reason: 'HTTPError: Could not convert value "170441" to type "bool" (for option "-size")'
},
{
name: 'stats a dag-cbor node',
reason: 'TODO go-ipfs does not support non-dag-pb nodes in mfs'
},
{
name: 'stats an identity CID',
reason: 'TODO go-ipfs does not support non-dag-pb nodes in mfs'
},
{
name: 'limits how many bytes to write to a file (Really large file)',
reason: 'TODO go-ipfs drops the connection'
}
]
.concat(isFirefox
? [{
name: 'overwrites start of a file without truncating (Really large file)',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'limits how many bytes to write to a file (Really large file)',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'pads the start of a new file when an offset is specified (Really large file)',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'expands a file when an offset is specified (Really large file)',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'expands a file when an offset is specified and the offset is longer than the file (Really large file)',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'truncates a file after writing (Really large file)',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'writes a file with raw blocks for newly created leaf nodes (Really large file)',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}]
: [])
})
tests.key(commonFactory, {
skip: [
// key.export
{
name: 'export',
reason: 'TODO not implemented in go-ipfs yet'
},
// key.import
{
name: 'import',
reason: 'TODO not implemented in go-ipfs yet'
}
]
})
tests.miscellaneous(commonFactory, {
skip: [
{
name: 'should include the interface-ipfs-core version',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should include the ipfs-http-client version',
reason: 'TODO not implemented in go-ipfs yet'
},
{
name: 'should have protocols property',
reason: 'TODO not implemented in go-ipfs yet'
}
]
})
tests.name(factory({
type: 'go',
ipfsOptions: {
offline: true
}
}))
tests.namePubsub(factory({
type: 'go',
ipfsOptions: {
EXPERIMENTAL: {
ipnsPubsub: true
}
}
}), {
skip: [
// name.pubsub.cancel
{
name: 'should cancel a subscription correctly returning true',
reason: 'go-ipfs is really slow for publishing and resolving ipns records, unless in offline mode'
},
// name.pubsub.subs
{
name: 'should get the list of subscriptions updated after a resolve',
reason: 'go-ipfs is really slow for publishing and resolving ipns records, unless in offline mode'
},
// name.pubsub
{
name: 'should publish and then resolve correctly',
reason: 'js-ipfs and go-ipfs behaviour differs'
},
{
name: 'should self resolve, publish and then resolve correctly',
reason: 'js-ipfs and go-ipfs behaviour differs'
},
{
name: 'should handle event on publish correctly',
reason: 'js-ipfs and go-ipfs behaviour differs'
}
]
})
tests.object(commonFactory, {
skip: [
{
name: 'should get data by base58 encoded multihash string',
reason: 'FIXME go-ipfs throws invalid encoding: base58'
},
{
name: 'should get object by base58 encoded multihash',
reason: 'FIXME go-ipfs throws invalid encoding: base58'
},
{
name: 'should get object by base58 encoded multihash',
reason: 'FIXME go-ipfs throws invalid encoding: base58'
},
{
name: 'should get object by base58 encoded multihash string',
reason: 'FIXME go-ipfs throws invalid encoding: base58'
},
{
name: 'should get links by base58 encoded multihash',
reason: 'FIXME go-ipfs throws invalid encoding: base58'
},
{
name: 'should get links by base58 encoded multihash string',
reason: 'FIXME go-ipfs throws invalid encoding: base58'
},
{
name: 'should put a Protobuf encoded Uint8Array',
reason: 'FIXME go-ipfs throws invalid encoding: protobuf'
}
]
.concat(isFirefox
? [{
name: 'should supply unaltered data',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}]
: [])
})
tests.pin(commonFactory, {
skip: [
{
name: 'should list pins with metadata',
reason: 'not implemented in go-ipfs'
}
]
})
tests.ping(commonFactory, {
skip: [
{
name: 'should fail when pinging a peer that is not available',
reason: 'FIXME go-ipfs return success with text: Looking up peer '
}
]
})
tests.pubsub(factory({
type: 'go'
}, {
go: {
args: ['--enable-pubsub-experiment']
}
}), {
skip: [{
name: 'should receive messages from a different node on lots of topics',
reason: 'HTTP clients cannot hold this many connections open'
}].concat(
isWindows
? [{
name: 'should send/receive 100 messages',
reason: 'FIXME https://github.com/ipfs/interface-ipfs-core/pull/188#issuecomment-354673246 and https://github.com/ipfs/go-ipfs/issues/4778'
},
{
name: 'should receive multiple messages',
reason: 'FIXME https://github.com/ipfs/interface-ipfs-core/pull/188#issuecomment-354673246 and https://github.com/ipfs/go-ipfs/issues/4778'
}]
: []
)
})
tests.repo(commonFactory)
tests.stats(commonFactory)
tests.swarm(commonFactory)
})
================================================
FILE: packages/ipfs/test/interface-http-js.js
================================================
/* eslint-env mocha */
import * as tests from 'interface-ipfs-core'
import { isNode, isBrowser, isWebWorker } from 'ipfs-utils/src/env.js'
import { factory } from './utils/factory.js'
const isFirefox = globalThis.navigator?.userAgent?.toLowerCase().includes('firefox')
/** @typedef { import("ipfsd-ctl").ControllerOptions } ControllerOptions */
describe('interface-ipfs-core over ipfs-http-client tests against js-ipfs', function () {
this.timeout(20000)
const commonFactory = factory({
type: 'js'
})
tests.root(commonFactory, {
skip: [
{
name: 'should support bidirectional streaming',
reason: 'Not supported by http'
},
{
name: 'should error during add-all stream',
reason: 'Not supported by http'
}]
.concat(isNode
? [{
name: 'should fail when passed invalid input',
reason: 'node-fetch cannot detect errors in streaming bodies - https://github.com/node-fetch/node-fetch/issues/753'
}, {
name: 'should not add from an invalid url',
reason: 'node-fetch cannot detect errors in streaming bodies - https://github.com/node-fetch/node-fetch/issues/753'
}]
: [{
name: 'should add with mtime as hrtime',
reason: 'Not designed to run in the browser'
}])
.concat(isFirefox
? [{
name: 'should add a BIG Uint8Array',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'should add a BIG Uint8Array with progress enabled',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'should add big files',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}]
: [])
})
tests.bitswap(commonFactory)
tests.block(commonFactory)
tests.bootstrap(commonFactory)
tests.config(commonFactory)
tests.dag(commonFactory, {
skip: [{
name: 'should get only a CID, due to resolving locally only',
reason: 'Local resolve option is not implemented yet'
}]
})
tests.dht(commonFactory)
tests.files(commonFactory, {
skip: (isBrowser || isWebWorker
? [{
name: 'should make directory and specify mtime as hrtime',
reason: 'Not designed to run in the browser'
}, {
name: 'should write file and specify mtime as hrtime',
reason: 'Not designed to run in the browser'
}, {
name: 'should set mtime as hrtime',
reason: 'Not designed to run in the browser'
}]
: [])
.concat(isFirefox
? [{
name: 'overwrites start of a file without truncating (Really large file)',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'limits how many bytes to write to a file (Really large file)',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'pads the start of a new file when an offset is specified (Really large file)',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'expands a file when an offset is specified (Really large file)',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'expands a file when an offset is specified and the offset is longer than the file (Really large file)',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'truncates a file after writing (Really large file)',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}, {
name: 'writes a file with raw blocks for newly created leaf nodes (Really large file)',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}]
: [])
})
tests.key(commonFactory)
tests.miscellaneous(commonFactory)
tests.name(factory({
type: 'js',
ipfsOptions: {
offline: true
}
}))
tests.namePubsub(factory({
type: 'js',
ipfsBin: './src/cli.js',
ipfsOptions: {
EXPERIMENTAL: {
ipnsPubsub: true
}
}
}))
tests.object(commonFactory, {
skip: isFirefox
? [{
name: 'should supply unaltered data',
reason: 'https://github.com/microsoft/playwright/issues/4704#issuecomment-826782602'
}]
: []
})
tests.pin(commonFactory, {
skip: [{
name: 'should throw an error on missing direct pins for existing path',
reason: 'FIXME: fetch does not yet support HTTP trailers https://github.com/ipfs/js-ipfs/issues/2519'
}, {
name: 'should throw an error on missing link for a specific path',
reason: 'FIXME: fetch does not yet support HTTP trailers https://github.com/ipfs/js-ipfs/issues/2519'
}, {
name: '.pin.remote.service',
reason: 'Not implemented'
}, {
name: '.pin.remote.add',
reason: 'Not implemented'
}, {
name: '.pin.remote.ls',
reason: 'Not implemented'
}, {
name: '.pin.remote.rm',
reason: 'Not implemented'
}, {
name: '.pin.remote.rmAll',
reason: 'Not implemented'
}]
})
tests.ping(commonFactory, {
skip: [{
name: 'should fail when pinging a peer that is not available',
reason: 'FIXME: fetch does not yet support HTTP trailers https://github.com/ipfs/js-ipfs/issues/2519'
}]
})
tests.pubsub(factory({
type: 'js'
}, {
go: {
args: ['--enable-pubsub-experiment']
}
}), {
skip: [{
name: 'should receive messages from a different node on lots of topics',
reason: 'HTTP clients cannot hold this many connections open'
}]
})
tests.repo(commonFactory)
tests.stats(commonFactory)
tests.swarm(commonFactory)
})
================================================
FILE: packages/ipfs/test/utils/factory.js
================================================
import { createFactory } from 'ipfsd-ctl'
import mergeOpts from 'merge-options'
import { isNode, isBrowser } from 'ipfs-utils/src/env.js'
import * as ipfsHttpModule from 'ipfs-http-client'
import * as ipfsModule from 'ipfs-core'
// @ts-expect-error no types
import goIpfs from 'go-ipfs'
import path, { dirname } from 'path'
import { fileURLToPath } from 'url'
import { webSockets } from '@libp2p/websockets'
import { all as WebSocketsFiltersAll } from '@libp2p/websockets/filters'
const merge = mergeOpts.bind({ ignoreUndefined: true })
let __dirname = ''
if (isNode) {
__dirname = dirname(fileURLToPath(import.meta.url))
}
const commonOptions = {
test: true,
type: 'proc',
ipfsHttpModule,
ipfsModule,
ipfsOptions: {
pass: 'ipfs-is-awesome-software',
libp2p: {
dialer: {
dialTimeout: 60e3 // increase timeout because travis is slow
},
transports: [
webSockets({
filter: WebSocketsFiltersAll
})
]
}
},
endpoint: process.env.IPFSD_SERVER
}
const commonOverrides = {
js: {
...(isNode
? {
ipfsBin: path.resolve(path.join(__dirname, '../../src/cli.js'))
}
: {}),
...(isBrowser
? {
remote: true
}
: {})
},
proc: {
...(isBrowser
? {
ipfsOptions: {
config: {
Addresses: {
Swarm: [
process.env.SIGNALA_SERVER
]
}
}
}
}
: {})
},
go: {
ipfsBin: isNode ? goIpfs.path() : undefined
}
}
export const factory = (options = {}, overrides = {}) => {
return createFactory(
merge(commonOptions, options),
merge(commonOverrides, overrides)
)
}
================================================
FILE: packages/ipfs/test/utils/mock-pinning-service.js
================================================
import http from 'http'
// @ts-expect-error no types
import { setup } from 'mock-ipfs-pinning-service'
import getPort from 'aegir/get-port'
const defaultPort = 1139
const defaultToken = 'secret'
export class PinningService {
/**
* @param {object} options
* @param {number} [options.port]
* @param {string|null} [options.token]
* @returns {Promise}
*/
static async start ({ port = defaultPort, token = defaultToken } = {}) {
const service = await setup({ token })
const server = http.createServer(service)
const host = '127.0.0.1'
port = await getPort(port)
await new Promise(resolve => server.listen(port, host, () => {
resolve(null)
}))
return new PinningService({ server, host, port, token })
}
/**
* @param {PinningService} service
* @returns {Promise}
*/
static stop (service) {
return new Promise((resolve, reject) => {
service.server.close((/** @type {any} */ error) => {
if (error) {
reject(error)
} else {
resolve()
}
})
})
}
/**
* @param {object} config
* @param {any} config.server
* @param {string} config.host
* @param {number} config.port
* @param {any} config.token
*/
constructor ({ server, host, port, token }) {
this.server = server
this.host = host
this.port = port
this.token = token
}
get endpoint () {
return `http://${this.host}:${this.port}`
}
}
================================================
FILE: packages/ipfs/test/utils/mock-preload-node.js
================================================
/* eslint-env browser */
import http from 'http'
import { URL } from 'iso-url'
import getPort from 'aegir/get-port'
export const defaultPort = 1138
export const defaultAddr = `/dnsaddr/localhost/tcp/${defaultPort}`
// Create a mock preload IPFS node with a gateway that'll respond 200 to a
// request for /api/v0/refs?arg=*. It remembers the preload CIDs it has been
// called with, and you can ask it for them and also clear them by issuing a
// GET/DELETE request to /cids.
export function createNode () {
/** @type {string[]} */
let cids = []
const server = http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Request-Method', '*')
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, DELETE')
res.setHeader('Access-Control-Allow-Headers', '*')
if (req.method === 'OPTIONS') {
res.writeHead(200)
res.end()
return
}
if (`${req.url}`.startsWith('/api/v0/refs')) {
const arg = new URL(`https://ipfs.io${req.url}`).searchParams.get('arg')
if (arg) {
cids.push(arg)
}
} else if (req.method === 'DELETE' && req.url === '/cids') {
res.statusCode = 204
cids = []
} else if (req.method === 'GET' && req.url === '/cids') {
res.setHeader('Content-Type', 'application/json')
res.write(JSON.stringify(cids))
} else {
res.statusCode = 500
}
res.end()
})
// @ts-expect-error
server.start = async () => {
const port = await getPort(defaultPort)
return new Promise((resolve, reject) => {
server.listen(port, '127.0.0.1', (/** @type {any} */ err) => {
if (err) {
return reject(err)
}
resolve(null)
})
})
}
// @ts-expect-error
server.stop = () => new Promise(resolve => server.close(resolve))
return server
}
================================================
FILE: packages/ipfs/tsconfig.json
================================================
{
"extends": "aegir/src/config/tsconfig.aegir.json",
"compilerOptions": {
"outDir": "dist",
"emitDeclarationOnly": true
},
"include": [
"src",
"test",
"package.json"
],
"references": [
{
"path": "../interface-ipfs-core"
},
{
"path": "../ipfs-cli"
},
{
"path": "../ipfs-client"
},
{
"path": "../ipfs-core"
},
{
"path": "../ipfs-core-types"
},
{
"path": "../ipfs-http-client"
}
]
}
================================================
FILE: packages/ipfs-cli/CHANGELOG.md
================================================
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
### [0.16.1](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.16.0...ipfs-cli-v0.16.1) (2023-05-25)
### Bug Fixes
* add deprecation notice to readmes ([#4362](https://www.github.com/ipfs/js-ipfs/issues/4362)) ([7b79c1b](https://www.github.com/ipfs/js-ipfs/commit/7b79c1b8df5c818dc124b346ea28330455732d5c))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core bumped from ^0.18.0 to ^0.18.1
* ipfs-core-types bumped from ^0.14.0 to ^0.14.1
* ipfs-core-utils bumped from ^0.18.0 to ^0.18.1
* ipfs-daemon bumped from ^0.16.0 to ^0.16.1
* ipfs-http-client bumped from ^60.0.0 to ^60.0.1
## [0.16.0](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.15.0...ipfs-cli-v0.16.0) (2023-01-11)
### ⚠ BREAKING CHANGES
* update multiformats to v11.x.x and related depenendcies (#4277)
### Bug Fixes
* update multiformats to v11.x.x and related depenendcies ([#4277](https://www.github.com/ipfs/js-ipfs/issues/4277)) ([521c84a](https://www.github.com/ipfs/js-ipfs/commit/521c84a958b04d61702577a5adce28519c1b2a3b))
* use aegir to publish RCs ([#4284](https://www.github.com/ipfs/js-ipfs/issues/4284)) ([6d90cbf](https://www.github.com/ipfs/js-ipfs/commit/6d90cbf321a7dbf4b1084ba20f0c514dc08d8d0a))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core bumped from ^0.17.0 to ^0.18.0
* ipfs-core-types bumped from ^0.13.0 to ^0.14.0
* ipfs-core-utils bumped from ^0.17.0 to ^0.18.0
* ipfs-daemon bumped from ^0.15.0 to ^0.16.0
* ipfs-http-client bumped from ^59.0.0 to ^60.0.0
## [0.15.0](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.14.2...ipfs-cli-v0.15.0) (2022-10-24)
### ⚠ BREAKING CHANGES
* ipfs is now bundled with libp2p@0.40.x which has different config
### Features
* upgrade libp2p to 0.40.x ([#4237](https://www.github.com/ipfs/js-ipfs/issues/4237)) ([0cee4a4](https://www.github.com/ipfs/js-ipfs/commit/0cee4a4c55767022584dcbade0b0b9b43326f9c9))
### Bug Fixes
* replace slice with subarray for increased performance ([#4210](https://www.github.com/ipfs/js-ipfs/issues/4210)) ([dfc43d4](https://www.github.com/ipfs/js-ipfs/commit/dfc43d4e9be67fdf25553677f469379d966ff806))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core bumped from ^0.16.1 to ^0.17.0
* ipfs-core-types bumped from ^0.12.1 to ^0.13.0
* ipfs-core-utils bumped from ^0.16.1 to ^0.17.0
* ipfs-daemon bumped from ^0.14.2 to ^0.15.0
* ipfs-http-client bumped from ^58.0.1 to ^59.0.0
### [0.14.2](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.14.1...ipfs-cli-v0.14.2) (2022-09-21)
### Bug Fixes
* update @multiformats/multiadd to 11.0.0 ([2a830bf](https://www.github.com/ipfs/js-ipfs/commit/2a830bf58a5929fcce51dede871c99f62192fbda))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core bumped from ^0.16.0 to ^0.16.1
* ipfs-core-types bumped from ^0.12.0 to ^0.12.1
* ipfs-core-utils bumped from ^0.16.0 to ^0.16.1
* ipfs-daemon bumped from ^0.14.1 to ^0.14.2
* ipfs-http-client bumped from ^58.0.0 to ^58.0.1
### [0.14.1](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.14.0...ipfs-cli-v0.14.1) (2022-09-16)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-daemon bumped from ^0.14.0 to ^0.14.1
## [0.14.0](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.13.5...ipfs-cli-v0.14.0) (2022-09-06)
### ⚠ BREAKING CHANGES
* update to libp2p@0.38.x (#4151)
### deps
* update to libp2p@0.38.x ([#4151](https://www.github.com/ipfs/js-ipfs/issues/4151)) ([39dbf70](https://www.github.com/ipfs/js-ipfs/commit/39dbf708ec31b263115e44f420651fa4e056a89e))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core bumped from ^0.15.0 to ^0.16.0
* ipfs-core-types bumped from ^0.11.0 to ^0.12.0
* ipfs-core-utils bumped from ^0.15.0 to ^0.16.0
* ipfs-daemon bumped from ^0.13.0 to ^0.14.0
* ipfs-http-client bumped from ^57.0.0 to ^58.0.0
### [0.13.5](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.13.4...ipfs-cli-v0.13.5) (2022-06-24)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core bumped from ^0.15.3 to ^0.15.4
* ipfs-daemon bumped from ^0.13.4 to ^0.13.5
* ipfs-http-client bumped from ^57.0.2 to ^57.0.3
### [0.13.4](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.13.3...ipfs-cli-v0.13.4) (2022-06-22)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core bumped from ^0.15.2 to ^0.15.3
* ipfs-core-types bumped from ^0.11.0 to ^0.11.1
* ipfs-core-utils bumped from ^0.15.0 to ^0.15.1
* ipfs-daemon bumped from ^0.13.3 to ^0.13.4
* ipfs-http-client bumped from ^57.0.1 to ^57.0.2
### [0.13.3](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.13.2...ipfs-cli-v0.13.3) (2022-06-13)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core bumped from ^0.15.1 to ^0.15.2
* ipfs-daemon bumped from ^0.13.2 to ^0.13.3
### [0.13.2](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.13.1...ipfs-cli-v0.13.2) (2022-06-01)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core bumped from ^0.15.0 to ^0.15.1
* ipfs-daemon bumped from ^0.13.1 to ^0.13.2
* ipfs-http-client bumped from ^57.0.0 to ^57.0.1
### [0.13.1](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.13.0...ipfs-cli-v0.13.1) (2022-05-30)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-daemon bumped from ^0.13.0 to ^0.13.1
## [0.13.0](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.12.3...ipfs-cli-v0.13.0) (2022-05-27)
### ⚠ BREAKING CHANGES
* This module is now ESM only and there return types of some methods have changed
### Features
* update to libp2p 0.37.x ([#4092](https://www.github.com/ipfs/js-ipfs/issues/4092)) ([74aee8b](https://www.github.com/ipfs/js-ipfs/commit/74aee8b3d78f233c3199a3e9a6c0ac628a31a433))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core bumped from ^0.14.3 to ^0.15.0
* ipfs-core-types bumped from ^0.10.3 to ^0.11.0
* ipfs-core-utils bumped from ^0.14.3 to ^0.15.0
* ipfs-daemon bumped from ^0.12.2 to ^0.13.0
* ipfs-http-client bumped from ^56.0.3 to ^57.0.0
### [0.12.3](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.12.2...ipfs-cli-v0.12.3) (2022-04-20)
### Bug Fixes
* upgrade dep of ipfs-utils ^9.0.2->^9.0.6 ([#4086](https://www.github.com/ipfs/js-ipfs/issues/4086)) ([8f7ce23](https://www.github.com/ipfs/js-ipfs/commit/8f7ce23c18be12bdc52b98bfccbd0a5a2a9c9f7e)), closes [#4080](https://www.github.com/ipfs/js-ipfs/issues/4080)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core bumped from ^0.14.2 to ^0.14.3
* ipfs-core-types bumped from ^0.10.2 to ^0.10.3
* ipfs-core-utils bumped from ^0.14.2 to ^0.14.3
* ipfs-daemon bumped from ^0.12.2 to ^0.12.3
* ipfs-http-client bumped from ^56.0.2 to ^56.0.3
### [0.12.2](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.12.1...ipfs-cli-v0.12.2) (2022-03-01)
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core bumped from ^0.14.1 to ^0.14.2
* ipfs-core-types bumped from ^0.10.1 to ^0.10.2
* ipfs-core-utils bumped from ^0.14.1 to ^0.14.2
* ipfs-daemon bumped from ^0.12.1 to ^0.12.2
* ipfs-http-client bumped from ^56.0.1 to ^56.0.2
### [0.12.1](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.12.0...ipfs-cli-v0.12.1) (2022-02-06)
### Bug Fixes
* **dag:** replace custom dag walk with multiformats/traversal ([#3950](https://www.github.com/ipfs/js-ipfs/issues/3950)) ([596b1f4](https://www.github.com/ipfs/js-ipfs/commit/596b1f48a014083b1736e4ad7e746c652d2583b1))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core bumped from ^0.14.0 to ^0.14.1
* ipfs-core-types bumped from ^0.10.0 to ^0.10.1
* ipfs-core-utils bumped from ^0.14.0 to ^0.14.1
* ipfs-daemon bumped from ^0.12.0 to ^0.12.1
* ipfs-http-client bumped from ^56.0.0 to ^56.0.1
## [0.12.0](https://www.github.com/ipfs/js-ipfs/compare/ipfs-cli-v0.11.0...ipfs-cli-v0.12.0) (2022-01-27)
### ⚠ BREAKING CHANGES
* peerstore methods are now all async, the repo is migrated to v12
### Features
* libp2p async peerstore ([#4018](https://www.github.com/ipfs/js-ipfs/issues/4018)) ([a6b201a](https://www.github.com/ipfs/js-ipfs/commit/a6b201af2c3697430ab0ebe002dd573d185f1ac0))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* ipfs-core bumped from ^0.13.0 to ^0.14.0
* ipfs-core-types bumped from ^0.9.0 to ^0.10.0
* ipfs-core-utils bumped from ^0.13.0 to ^0.14.0
* ipfs-daemon bumped from ^0.11.0 to ^0.12.0
* ipfs-http-client bumped from ^55.0.0 to ^56.0.0
## [0.11.0](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.10.2...ipfs-cli@0.11.0) (2021-12-15)
### Bug Fixes
* ensure directory is passed ([#3968](https://github.com/ipfs/js-ipfs/issues/3968)) ([80ac58c](https://github.com/ipfs/js-ipfs/commit/80ac58ca27cc9f21823a23d1e6357f738fdb6781))
* **pubsub:** multibase in pubsub http rpc ([#3922](https://github.com/ipfs/js-ipfs/issues/3922)) ([6eeaca4](https://github.com/ipfs/js-ipfs/commit/6eeaca452c36fa13be42d704575c577e4ca938f1))
### chore
* Bump @ipld/dag-cbor to v7 ([#3977](https://github.com/ipfs/js-ipfs/issues/3977)) ([73476f5](https://github.com/ipfs/js-ipfs/commit/73476f55e39ecfb01eb2b4880637aad658f51bc2))
### Features
* dht client ([#3947](https://github.com/ipfs/js-ipfs/issues/3947)) ([62d8ecb](https://github.com/ipfs/js-ipfs/commit/62d8ecbc723e693a2544e69172d99c576d187c23))
* update DAG API to match go-ipfs@0.10 changes ([#3917](https://github.com/ipfs/js-ipfs/issues/3917)) ([38c01be](https://github.com/ipfs/js-ipfs/commit/38c01be03b4fd5f401cd9b698cfdb4237d835b01))
### BREAKING CHANGES
* **pubsub:** We had to make breaking changes to `pubsub` commands sent over HTTP RPC to fix data corruption caused by topic names and payload bytes that included `\n`. More details in https://github.com/ipfs/go-ipfs/issues/7939 and https://github.com/ipfs/go-ipfs/pull/8183
* On decode of CBOR blocks, `undefined` values will be coerced to `null`
* `ipfs.dag.put` no longer accepts a `format` arg, it is now `storeCodec` and `inputCodec`. `'json'` has become `'dag-json'`, `'cbor'` has become `'dag-cbor'` and so on
* The DHT API has been refactored to return async iterators of query events
## [0.10.2](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.10.1...ipfs-cli@0.10.2) (2021-11-24)
**Note:** Version bump only for package ipfs-cli
## [0.10.1](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.10.0...ipfs-cli@0.10.1) (2021-11-19)
**Note:** Version bump only for package ipfs-cli
## [0.10.0](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.9.1...ipfs-cli@0.10.0) (2021-11-12)
### Bug Fixes
* do not accept single items for ipfs.add ([#3900](https://github.com/ipfs/js-ipfs/issues/3900)) ([04e3cf3](https://github.com/ipfs/js-ipfs/commit/04e3cf3f46b585c4644cba70516f375e95361f52))
### BREAKING CHANGES
* errors will now be thrown if multiple items are passed to `ipfs.add` or single items to `ipfs.addAll` (n.b. you can still pass a list of a single item to `ipfs.addAll`)
### [0.9.1](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.9.0...ipfs-cli@0.9.1) (2021-09-28)
**Note:** Version bump only for package ipfs-cli
## [0.9.0](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.8.8...ipfs-cli@0.9.0) (2021-09-24)
### Features
* pull in new globSource ([#3889](https://github.com/ipfs/js-ipfs/issues/3889)) ([be4a542](https://github.com/ipfs/js-ipfs/commit/be4a5428ebc4b05a2edd9a91bf9df6416c1a8c2b))
* switch to esm ([#3879](https://github.com/ipfs/js-ipfs/issues/3879)) ([9a40109](https://github.com/ipfs/js-ipfs/commit/9a40109632e5b4837eb77a2f57dbc77fbf1fe099))
### BREAKING CHANGES
* the globSource api has changed from `globSource(dir, opts)` to `globSource(dir, pattern, opts)`
* There are no default exports and everything is now dual published as ESM/CJS
### [0.8.8](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.8.7...ipfs-cli@0.8.8) (2021-09-17)
**Note:** Version bump only for package ipfs-cli
### [0.8.7](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.8.6...ipfs-cli@0.8.7) (2021-09-17)
**Note:** Version bump only for package ipfs-cli
### [0.8.6](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.8.5...ipfs-cli@0.8.6) (2021-09-08)
**Note:** Version bump only for package ipfs-cli
### [0.8.5](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.8.4...ipfs-cli@0.8.5) (2021-09-02)
### Bug Fixes
* declare types in .ts files ([#3840](https://github.com/ipfs/js-ipfs/issues/3840)) ([eba5fe6](https://github.com/ipfs/js-ipfs/commit/eba5fe6832858107b3e1ae02c99de674622f12b4))
* remove use of instanceof for CID class ([#3847](https://github.com/ipfs/js-ipfs/issues/3847)) ([ebbb12d](https://github.com/ipfs/js-ipfs/commit/ebbb12db523c53ce8e4ddae5266cd9acb3504431))
### [0.8.4](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.8.3...ipfs-cli@0.8.4) (2021-08-25)
### Bug Fixes
* grpc server may not be enabled ([#3834](https://github.com/ipfs/js-ipfs/issues/3834)) ([533845e](https://github.com/ipfs/js-ipfs/commit/533845e3d140459ca383b1538e571d08850c0ef8))
### [0.8.3](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.8.1...ipfs-cli@0.8.3) (2021-08-17)
**Note:** Version bump only for package ipfs-cli
### [0.8.1](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.8.0...ipfs-cli@0.8.1) (2021-08-17)
### Bug Fixes
* pin nanoid version ([#3807](https://github.com/ipfs/js-ipfs/issues/3807)) ([474523a](https://github.com/ipfs/js-ipfs/commit/474523ab8702729f697843d433a7a08baf2d101f))
## [0.8.0](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.7.1...ipfs-cli@0.8.0) (2021-08-11)
### Features
* ed25519 keys by default ([#3693](https://github.com/ipfs/js-ipfs/issues/3693)) ([33fa734](https://github.com/ipfs/js-ipfs/commit/33fa7341c3baaf0926d887c071cc6fbce5ac49a8))
* make ipfs.get output tarballs ([#3785](https://github.com/ipfs/js-ipfs/issues/3785)) ([1ad6001](https://github.com/ipfs/js-ipfs/commit/1ad60018d39d5b46c484756631e30e1989fd8eba))
### BREAKING CHANGES
* the output type of `ipfs.get` has changed and the `recursive` option has been removed from `ipfs.ls` since it was not supported everywhere
### [0.7.1](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.7.0...ipfs-cli@0.7.1) (2021-07-30)
**Note:** Version bump only for package ipfs-cli
## [0.7.0](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.6.2...ipfs-cli@0.7.0) (2021-07-27)
### Bug Fixes
* make "ipfs resolve" cli command recursive by default ([#3707](https://github.com/ipfs/js-ipfs/issues/3707)) ([399ce36](https://github.com/ipfs/js-ipfs/commit/399ce367a1dbc531b52fe228ee4212008c9a1091)), closes [#3692](https://github.com/ipfs/js-ipfs/issues/3692)
### Features
* implement dag import/export ([#3728](https://github.com/ipfs/js-ipfs/issues/3728)) ([700765b](https://github.com/ipfs/js-ipfs/commit/700765be2634fa5d2d71d8b87cf68c9cd328d2c4)), closes [#2953](https://github.com/ipfs/js-ipfs/issues/2953) [#2745](https://github.com/ipfs/js-ipfs/issues/2745)
* upgrade to the new multiformats ([#3556](https://github.com/ipfs/js-ipfs/issues/3556)) ([d13d15f](https://github.com/ipfs/js-ipfs/commit/d13d15f022a87d04a35f0f7822142f9cb898479c))
### BREAKING CHANGES
* resolve is now recursive by default
Co-authored-by: Alex Potsides
* ipld-formats no longer supported, use multiformat BlockCodecs instead
Co-authored-by: Rod Vagg
Co-authored-by: achingbrain
### [0.6.2](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.6.1...ipfs-cli@0.6.2) (2021-06-18)
**Note:** Version bump only for package ipfs-cli
### [0.6.1](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.6.0...ipfs-cli@0.6.1) (2021-06-05)
### Bug Fixes
* stalling subscription on (node) http-client when daemon is stopped ([#3468](https://github.com/ipfs/js-ipfs/issues/3468)) ([0266abf](https://github.com/ipfs/js-ipfs/commit/0266abf0c4b817636172f78c6e91eb4dd5aad451)), closes [#3465](https://github.com/ipfs/js-ipfs/issues/3465)
## [0.6.0](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.5.1...ipfs-cli@0.6.0) (2021-05-26)
### Features
* allow passing the id of a network peer to ipfs.id ([#3386](https://github.com/ipfs/js-ipfs/issues/3386)) ([00fd709](https://github.com/ipfs/js-ipfs/commit/00fd709a7b71e7cf354ea452ebce460dd7375d34))
### [0.5.1](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.5.0...ipfs-cli@0.5.1) (2021-05-11)
**Note:** Version bump only for package ipfs-cli
## [0.5.0](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.4.4...ipfs-cli@0.5.0) (2021-05-10)
### Bug Fixes
* mark ipld options as partial ([#3669](https://github.com/ipfs/js-ipfs/issues/3669)) ([f98af8e](https://github.com/ipfs/js-ipfs/commit/f98af8ed24784929898bb5d33a64dc442c77074d))
* update ipfs repo ([#3671](https://github.com/ipfs/js-ipfs/issues/3671)) ([9029ee5](https://github.com/ipfs/js-ipfs/commit/9029ee591fa74ea65c9600f2d249897e933416fa))
* update types after feedback from ceramic ([#3657](https://github.com/ipfs/js-ipfs/issues/3657)) ([0ddbb1b](https://github.com/ipfs/js-ipfs/commit/0ddbb1b1deb4e40dac3e365d7f98a5f174c2ce8f)), closes [#3640](https://github.com/ipfs/js-ipfs/issues/3640)
### chore
* upgrade deps with new typedefs ([#3550](https://github.com/ipfs/js-ipfs/issues/3550)) ([a418a52](https://github.com/ipfs/js-ipfs/commit/a418a521574c878d7aabd0ad2fd8d516908a3756))
### BREAKING CHANGES
* all core api methods now have types, some method signatures have changed, named exports are now used by the http, grpc and ipfs client modules
### [0.4.4](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.4.3...ipfs-cli@0.4.4) (2021-03-10)
**Note:** Version bump only for package ipfs-cli
### [0.4.3](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.4.2...ipfs-cli@0.4.3) (2021-03-09)
### Bug Fixes
* update to new aegir ([#3528](https://github.com/ipfs/js-ipfs/issues/3528)) ([49f7880](https://github.com/ipfs/js-ipfs/commit/49f78807d7e26483bd926b45cc7e0f797d77e41b))
### [0.4.2](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.4.1...ipfs-cli@0.4.2) (2021-02-08)
**Note:** Version bump only for package ipfs-cli
### [0.4.1](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.4.0...ipfs-cli@0.4.1) (2021-02-02)
**Note:** Version bump only for package ipfs-cli
## [0.4.0](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.3.2...ipfs-cli@0.4.0) (2021-02-01)
### Bug Fixes
* updates webpack example to use v5 ([#3512](https://github.com/ipfs/js-ipfs/issues/3512)) ([c7110db](https://github.com/ipfs/js-ipfs/commit/c7110db71b5c0f0f9f415f31f91b5b228341e13e)), closes [#3511](https://github.com/ipfs/js-ipfs/issues/3511)
### chore
* update deps ([#3514](https://github.com/ipfs/js-ipfs/issues/3514)) ([061d77c](https://github.com/ipfs/js-ipfs/commit/061d77cc03f40af5a3bc3590481e1e5836e7f0d8))
### BREAKING CHANGES
* ipfs-repo upgrade requires repo migration to v10
### [0.3.2](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.3.1...ipfs-cli@0.3.2) (2021-01-22)
**Note:** Version bump only for package ipfs-cli
### [0.3.1](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.3.0...ipfs-cli@0.3.1) (2021-01-20)
**Note:** Version bump only for package ipfs-cli
## [0.3.0](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.2.3...ipfs-cli@0.3.0) (2021-01-15)
### Features
* add grpc server and client ([#3403](https://github.com/ipfs/js-ipfs/issues/3403)) ([a9027e0](https://github.com/ipfs/js-ipfs/commit/a9027e0ec0cea9a4f34b4f2f52e09abb35237384)), closes [#2519](https://github.com/ipfs/js-ipfs/issues/2519) [#2838](https://github.com/ipfs/js-ipfs/issues/2838) [#2943](https://github.com/ipfs/js-ipfs/issues/2943) [#2854](https://github.com/ipfs/js-ipfs/issues/2854) [#2864](https://github.com/ipfs/js-ipfs/issues/2864)
* allow passing a http.Agent to the grpc client ([#3477](https://github.com/ipfs/js-ipfs/issues/3477)) ([c5f0bc5](https://github.com/ipfs/js-ipfs/commit/c5f0bc5eeee15369b7d02901035b04184a8608d2)), closes [#3474](https://github.com/ipfs/js-ipfs/issues/3474)
### [0.2.3](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.2.2...ipfs-cli@0.2.3) (2020-12-16)
### Bug Fixes
* regressions introduced by new releases of CID & multicodec ([#3442](https://github.com/ipfs/js-ipfs/issues/3442)) ([b5152d8](https://github.com/ipfs/js-ipfs/commit/b5152d8cc93ecc8d39fc353ea66d7eaf1661e3c0)), closes [/github.com/multiformats/js-cid/commit/0e11f035c9230e7f6d79c159ace9b80de88cb5eb#diff-25a6634263c1b1f6fc4697a04e2b9904ea4b042a89af59dc93ec1f5d44848a26](https://github.com//github.com/multiformats/js-cid/commit/0e11f035c9230e7f6d79c159ace9b80de88cb5eb/issues/diff-25a6634263c1b1f6fc4697a04e2b9904ea4b042a89af59dc93ec1f5d44848a26)
### [0.2.2](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.2.1...ipfs-cli@0.2.2) (2020-11-25)
### Bug Fixes
* do not write to prefix outside of output directory ([#3417](https://github.com/ipfs/js-ipfs/issues/3417)) ([75dd865](https://github.com/ipfs/js-ipfs/commit/75dd86529650b039be21b05b92a6413269baa4ab))
* strip control characters from user output ([#3420](https://github.com/ipfs/js-ipfs/issues/3420)) ([d13b064](https://github.com/ipfs/js-ipfs/commit/d13b064882751b00c48d42aeb309131fde0dd5c8))
### [0.2.1](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.2.0...ipfs-cli@0.2.1) (2020-11-16)
### Bug Fixes
* correct raw leaves setting ([#3401](https://github.com/ipfs/js-ipfs/issues/3401)) ([c0703ef](https://github.com/ipfs/js-ipfs/commit/c0703ef78626a91186e0c7c3374584283367c064))
* report ipfs.add progress over http ([#3310](https://github.com/ipfs/js-ipfs/issues/3310)) ([39cad4b](https://github.com/ipfs/js-ipfs/commit/39cad4b76b950ea6a76477fd01f8631b8bd9aa1e))
## [0.2.0](https://github.com/ipfs/js-ipfs/compare/ipfs-cli@0.1.0...ipfs-cli@0.2.0) (2020-11-09)
### Bug Fixes
* remove electron-webrtc dependency ([#3378](https://github.com/ipfs/js-ipfs/issues/3378)) ([2bd5368](https://github.com/ipfs/js-ipfs/commit/2bd53686003527a102db9df92cedad4c6d9164f9)), closes [#3376](https://github.com/ipfs/js-ipfs/issues/3376)
### BREAKING CHANGES
* electron-webrtc was accidentally bundled with ipfs, now it needs installing separately
# 0.1.0 (2020-10-28)
### Bug Fixes
* error invalid version triggered in cli pin add/rm ([#3306](https://github.com/ipfs/js-ipfs/issues/3306)) ([69757f3](https://github.com/ipfs/js-ipfs/commit/69757f3c321c5d135ebde7a262c169427e4f1105)), closes [/github.com/ipfs/js-ipfs/blob/master/docs/core-api/PIN.md#returns-1](https://github.com//github.com/ipfs/js-ipfs/blob/master/docs/core-api/PIN.md/issues/returns-1)
* use fetch in electron renderer and electron-fetch in main ([#3251](https://github.com/ipfs/js-ipfs/issues/3251)) ([639d71f](https://github.com/ipfs/js-ipfs/commit/639d71f7ac8f66d9633e753a2a6be927e14a5af0))
### Features
* enable custom formats for dag put and get ([#3347](https://github.com/ipfs/js-ipfs/issues/3347)) ([3250ff4](https://github.com/ipfs/js-ipfs/commit/3250ff453a1d3275cc4ab746f59f9f70abd5cc5f))
* type check & generate defs from jsdoc ([#3281](https://github.com/ipfs/js-ipfs/issues/3281)) ([bbcaf34](https://github.com/ipfs/js-ipfs/commit/bbcaf34111251b142273a5675f4754ff68bd9fa0))
================================================
FILE: packages/ipfs-cli/CODE_OF_CONDUCT.md
================================================
# Contributor Code of Conduct
The `js-ipfs` project follows the [`IPFS Community Code of Conduct`](https://github.com/ipfs/community/blob/master/code-of-conduct.md)
================================================
FILE: packages/ipfs-cli/CONTRIBUTING.md
================================================
# Contributing guidelines
IPFS as a project, including js-ipfs and all of its modules, follows the [standard IPFS Community contributing guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md).
We also adhere to the [IPFS JavaScript Community contributing guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) which provide additional information of how to collaborate and contribute in the JavaScript implementation of IPFS.
We appreciate your time and attention for going over these. Please open an issue on [ipfs/community](https://github.com/ipfs/community) if you have any question.
Thank you.
================================================
FILE: packages/ipfs-cli/COPYRIGHT
================================================
This project is transitioning from an MIT-only license to a dual MIT/Apache-2.0 license.
Unless otherwise noted, all code contributed prior to 2019-11-21 and not contributed by
a user listed in [this signoff issue](https://github.com/ipfs/js-ipfs/issues/2624) is
licensed under MIT-only. All new contributions (and past contributions since 2019-11-21)
are licensed under a dual MIT/Apache-2.0 license.
================================================
FILE: packages/ipfs-cli/LICENSE
================================================
This project is dual licensed under MIT and Apache-2.0.
MIT: https://www.opensource.org/licenses/mit
Apache-2.0: https://www.apache.org/licenses/license-2.0
================================================
FILE: packages/ipfs-cli/LICENSE-APACHE
================================================
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
================================================
FILE: packages/ipfs-cli/LICENSE-MIT
================================================
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: packages/ipfs-cli/README.md
================================================
> # ⛔️ DEPRECATED: [js-IPFS](https://github.com/ipfs/js-ipfs) has been superseded by [Helia](https://github.com/ipfs/helia)
>
> 📚 [Learn more about this deprecation](https://github.com/ipfs/js-ipfs/issues/4336) or [how to migrate](https://github.com/ipfs/helia/wiki/Migrating-from-js-IPFS)
>
> ⚠️ If you continue using this repo, please note that security fixes will not be provided
# ipfs-cli
[](https://ipfs.tech)
[](https://discuss.ipfs.tech)
[](https://codecov.io/gh/ipfs/js-ipfs)
[](https://github.com/ipfs/js-ipfs/actions/workflows/test.yml?query=branch%3Amaster)
> JavaScript implementation of the IPFS specification
## Table of contents
- [Install](#install)
- [License](#license)
- [Contribute](#contribute)
## Install
```console
$ npm i ipfs-cli
```
```console
$ npm install -g ipfs
// npm install output
$ jsipfs daemon
```
## License
Licensed under either of
- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / )
- MIT ([LICENSE-MIT](LICENSE-MIT) / )
## Contribute
Contributions welcome! Please check out [the issues](https://github.com/ipfs/js-ipfs/issues).
Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general.
Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md).
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
[](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md)
================================================
FILE: packages/ipfs-cli/package.json
================================================
{
"name": "ipfs-cli",
"version": "0.16.1",
"description": "JavaScript implementation of the IPFS specification",
"license": "Apache-2.0 OR MIT",
"homepage": "https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs-cli#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/ipfs/js-ipfs.git"
},
"bugs": {
"url": "https://github.com/ipfs/js-ipfs/issues"
},
"keywords": [
"IPFS"
],
"engines": {
"node": ">=16.0.0",
"npm": ">=7.0.0"
},
"type": "module",
"types": "./dist/src/index.d.ts",
"typesVersions": {
"*": {
"*": [
"*",
"dist/*",
"dist/src/*",
"dist/src/*/index"
],
"src/*": [
"*",
"dist/*",
"dist/src/*",
"dist/src/*/index"
]
}
},
"files": [
"src",
"dist",
"!dist/test",
"!**/*.tsbuildinfo"
],
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./src/index.js"
},
"./utils": {
"types": "./src/utils.d.ts",
"import": "./src/utils.js"
}
},
"eslintConfig": {
"extends": "ipfs",
"parserOptions": {
"sourceType": "module"
}
},
"scripts": {
"lint": "aegir lint",
"test": "aegir test -t node --cov",
"test:node": "aegir test -t node --cov",
"clean": "aegir clean",
"dep-check": "aegir dep-check -i ipfs-core-types",
"build": "aegir build --no-bundle"
},
"dependencies": {
"@ipld/dag-cbor": "^9.0.0",
"@ipld/dag-json": "^10.0.0",
"@ipld/dag-pb": "^4.0.0",
"@libp2p/logger": "^2.0.5",
"@libp2p/peer-id": "^2.0.0",
"@multiformats/mafmt": "^11.0.2",
"@multiformats/multiaddr": "^11.1.5",
"@multiformats/multiaddr-to-uri": "^9.0.1",
"byteman": "^1.3.5",
"execa": "^6.1.0",
"get-folder-size": "^4.0.0",
"ipfs-core": "^0.18.1",
"ipfs-core-types": "^0.14.1",
"ipfs-core-utils": "^0.18.1",
"ipfs-daemon": "^0.16.1",
"ipfs-http-client": "^60.0.1",
"ipfs-utils": "^9.0.13",
"it-concat": "^3.0.1",
"it-merge": "^2.0.0",
"it-pipe": "^2.0.3",
"it-split": "^2.0.0",
"it-tar": "^6.0.0",
"jsondiffpatch": "^0.4.1",
"multiformats": "^11.0.0",
"parse-duration": "^1.0.0",
"pretty-bytes": "^6.0.0",
"progress": "^2.0.3",
"stream-to-it": "^0.2.2",
"uint8arrays": "^4.0.2",
"yargs": "^17.4.0"
},
"devDependencies": {
"@libp2p/crypto": "^1.0.7",
"@types/get-folder-size": "^3.0.1",
"@types/ncp": "^2.0.5",
"@types/progress": "^2.0.3",
"@types/rimraf": "^3.0.1",
"@types/yargs": "^17.0.10",
"aegir": "^37.11.0",
"ipfs-repo": "^17.0.0",
"it-all": "^2.0.0",
"it-first": "^2.0.0",
"it-map": "^2.0.0",
"it-to-buffer": "^3.0.0",
"nanoid": "^4.0.0",
"ncp": "^2.0.0",
"pako": "^2.0.4",
"rimraf": "^3.0.2",
"sinon": "^15.0.1",
"string-argv": "^0.3.1",
"temp-write": "^5.0.0"
}
}
================================================
FILE: packages/ipfs-cli/src/command-alias.js
================================================
/**
* @type {Record}
*/
const aliases = {
// We need to be able to show help text for both the `refs` command and the
// `refs local` command, but with yargs `refs` cannot be both a command and
// a command directory. So alias `refs local` to `refs-local`
'refs-local': ['refs', 'local']
}
/**
* Replace multi-word command with alias
* eg replace `refs local` with `refs-local`
*
* @param {string[]} args
*/
export default function (args) {
for (const [alias, original] of Object.entries(aliases)) {
if (arrayMatch(args, original)) {
return [alias, ...args.slice(original.length)]
}
}
return args
}
/**
* eg arrayMatch([1, 2, 3], [1, 2]) => true
*
* @param {string[]} arr
* @param {string[]} sub
*/
function arrayMatch (arr, sub) {
if (sub.length > arr.length) {
return false
}
for (let i = 0; i < sub.length; i++) {
if (arr[i] !== sub[i]) {
return false
}
}
return true
}
================================================
FILE: packages/ipfs-cli/src/commands/add.js
================================================
/* eslint-disable complexity */
import getFolderSize from 'get-folder-size'
// @ts-expect-error no types
import byteman from 'byteman'
import {
createProgressBar,
coerceMtime,
coerceMtimeNsecs,
stripControlCharacters
} from '../utils.js'
import globSource from 'ipfs-utils/src/files/glob-source.js'
import parseDuration from 'parse-duration'
import merge from 'it-merge'
import fs from 'fs'
import path from 'path'
/**
* @param {string[]} paths
*/
async function getTotalBytes (paths) {
const sizes = await Promise.all(paths.map(p => getFolderSize(p)))
return sizes.reduce((total, { size }) => total + size, 0)
}
/**
* @param {string} target
* @param {object} options
* @param {boolean} [options.recursive]
* @param {boolean} [options.hidden]
* @param {boolean} [options.preserveMode]
* @param {boolean} [options.preserveMtime]
* @param {number} [options.mode]
* @param {import('ipfs-unixfs').MtimeLike} [options.mtime]
*/
async function * getSource (target, options = {}) {
const absolutePath = path.resolve(target)
const stats = await fs.promises.stat(absolutePath)
if (stats.isFile()) {
let mtime = options.mtime
let mode = options.mode
if (options.preserveMtime) {
mtime = stats.mtime
}
if (options.preserveMode) {
mode = stats.mode
}
yield {
path: path.basename(target),
content: fs.createReadStream(absolutePath),
mtime,
mode
}
return
}
const dirName = path.basename(absolutePath)
let pattern = '*'
if (options.recursive) {
pattern = '**/*'
}
for await (const content of globSource(target, pattern, {
hidden: options.hidden,
preserveMode: options.preserveMode,
preserveMtime: options.preserveMtime,
mode: options.mode,
mtime: options.mtime
})) {
yield {
...content,
path: `${dirName}${content.path}`
}
}
}
/**
* @typedef {object} Argv
* @property {import('../types').Context} Argv.ctx
* @property {boolean} Argv.trickle
* @property {number} Argv.shardSplitThreshold
* @property {import('multiformats/cid').Version} Argv.cidVersion
* @property {boolean} Argv.rawLeaves
* @property {boolean} Argv.onlyHash
* @property {string} Argv.hash
* @property {boolean} Argv.wrapWithDirectory
* @property {boolean} Argv.pin
* @property {string} Argv.chunker
* @property {boolean} Argv.preload
* @property {number} Argv.fileImportConcurrency
* @property {number} Argv.blockWriteConcurrency
* @property {number} Argv.timeout
* @property {boolean} Argv.quieter
* @property {boolean} Argv.quiet
* @property {boolean} Argv.silent
* @property {boolean} Argv.progress
* @property {string[]} Argv.file
* @property {number} Argv.mtime
* @property {number} Argv.mtimeNsecs
* @property {boolean} Argv.recursive
* @property {boolean} Argv.hidden
* @property {boolean} Argv.preserveMode
* @property {boolean} Argv.preserveMtime
* @property {number} Argv.mode
* @property {string} Argv.cidBase
* @property {boolean} Argv.enableShardingExperiment
*/
/** @type {import('yargs').CommandModule} */
const command = {
command: 'add [file...]',
describe: 'Add a file to IPFS using the UnixFS data format',
builder: {
progress: {
alias: 'p',
boolean: true,
default: true,
describe: 'Stream progress data'
},
recursive: {
alias: 'r',
boolean: true,
default: false
},
trickle: {
alias: 't',
boolean: true,
default: false,
describe: 'Use the trickle DAG builder'
},
'wrap-with-directory': {
alias: 'w',
boolean: true,
default: false,
describe: 'Add a wrapping node'
},
'only-hash': {
alias: 'n',
boolean: true,
default: false,
describe: 'Only chunk and hash, do not write'
},
'block-write-concurrency': {
number: true,
default: 10,
describe: 'After a file has been chunked, this controls how many chunks to hash and add to the block store concurrently'
},
chunker: {
default: 'size-262144',
describe: 'Chunking algorithm to use, formatted like [size-{size}, rabin, rabin-{avg}, rabin-{min}-{avg}-{max}]'
},
'file-import-concurrency': {
number: true,
default: 50,
describe: 'How many files to import at once'
},
'enable-sharding-experiment': {
boolean: true,
default: false
},
'shard-split-threshold': {
number: true,
default: 1000
},
'raw-leaves': {
boolean: true,
describe: 'Use raw blocks for leaf nodes. (experimental)'
},
'cid-version': {
number: true,
describe: 'CID version. Defaults to 0 unless an option that depends on CIDv1 is passed. (experimental)',
default: 0
},
'cid-base': {
describe: 'Number base to display CIDs in',
string: true,
default: 'base58btc'
},
hash: {
string: true,
describe: 'Hash function to use. Will set CID version to 1 if used. (experimental)',
default: 'sha2-256'
},
quiet: {
alias: 'q',
boolean: true,
default: false,
describe: 'Write minimal output'
},
quieter: {
alias: 'Q',
boolean: true,
default: false,
describe: 'Write only final hash'
},
silent: {
boolean: true,
default: false,
describe: 'Write no output'
},
pin: {
boolean: true,
default: true,
describe: 'Pin this object when adding'
},
preload: {
boolean: true,
default: true,
describe: 'Preload this object when adding'
},
hidden: {
alias: 'H',
boolean: true,
default: false,
describe: 'Include files that are hidden. Only takes effect on recursive add.'
},
'preserve-mode': {
boolean: true,
default: false,
describe: 'Apply permissions to created UnixFS entries'
},
'preserve-mtime': {
boolean: true,
default: false,
describe: 'Apply modification time to created UnixFS entries'
},
mode: {
string: true,
describe: 'File mode to apply to created UnixFS entries'
},
mtime: {
number: true,
coerce: coerceMtime,
describe: 'Modification time in seconds before or since the Unix Epoch to apply to created UnixFS entries'
},
'mtime-nsecs': {
number: true,
coerce: coerceMtimeNsecs,
describe: 'Modification time fraction in nanoseconds'
},
timeout: {
string: true,
coerce: parseDuration
}
},
async handler ({
ctx: { ipfs, print, isDaemon, getStdin },
trickle,
shardSplitThreshold,
cidVersion,
rawLeaves,
onlyHash,
hash,
wrapWithDirectory,
pin,
chunker,
preload,
fileImportConcurrency,
blockWriteConcurrency,
timeout,
quieter,
quiet,
silent,
progress,
file,
mtime,
mtimeNsecs,
recursive,
hidden,
preserveMode,
preserveMtime,
mode,
cidBase,
enableShardingExperiment
}) {
const options = {
trickle,
shardSplitThreshold,
cidVersion,
rawLeaves,
onlyHash,
hashAlg: hash,
wrapWithDirectory,
pin,
chunker,
preload,
fileImportConcurrency,
blockWriteConcurrency,
/**
* @type {import('ipfs-core-types/src/root').AddProgressFn}
*/
progress: (bytes, name) => {},
timeout
}
if (enableShardingExperiment && isDaemon) {
throw new Error('Error: Enabling the sharding experiment should be done on the daemon')
}
/** @type {{update: Function, interrupt: Function, terminate: Function} | undefined} */
let bar
let log = print
if (quieter || quiet || silent) {
progress = false
}
if (progress && file) {
const totalBytes = await getTotalBytes(file)
bar = createProgressBar(totalBytes, print)
if (print.isTTY) {
// bar.interrupt uses clearLine and cursorTo methods that are only on TTYs
log = bar.interrupt.bind(bar)
}
/**
* @param {number} byteLength
*/
options.progress = byteLength => {
if (bar) {
bar.update(byteLength / totalBytes, { progress: byteman(byteLength, 2, 'MB') })
}
}
}
if (options.rawLeaves == null) {
options.rawLeaves = cidVersion > 0
}
/** @type {{ secs: number, nsecs?: number } | undefined} */
let date
if (mtime) {
date = { secs: mtime, nsecs: mtimeNsecs }
}
const source = file
? merge(...file.map(file => getSource(file, {
hidden,
recursive,
preserveMode,
preserveMtime,
mode,
mtime: date
})))
: [{
content: getStdin(),
mode,
mtime: date
}] // Pipe to ipfs.add tagging with mode and mtime
let finalCid
const base = await ipfs.bases.getBase(cidBase)
try {
for await (const { cid, path } of ipfs.addAll(source, options)) {
if (silent) {
continue
}
if (quieter) {
finalCid = cid
continue
}
const pathStr = stripControlCharacters(path)
const cidStr = cid.toString(base.encoder)
let message = cidStr
if (!quiet) {
// print the hash twice if we are piping from stdin
message = `added ${cidStr} ${file ? pathStr || '' : cidStr}`.trim()
}
log(message)
}
} catch (/** @type {any} */ err) {
// Tweak the error message and add more relevant info for the CLI
if (err.code === 'ERR_DIR_NON_RECURSIVE') {
err.message = `'${err.path}' is a directory, use the '-r' flag to specify directories`
}
throw err
} finally {
if (bar) {
bar.terminate()
}
}
if (quieter && finalCid) {
log(finalCid.toString(base.encoder))
}
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/bitswap/index.js
================================================
import bitswapStat from './stat.js'
import bitswapUnwant from './unwant.js'
import bitswapWantlist from './wantlist.js'
/** @type {import('yargs').CommandModule[]} */
export const commands = [
bitswapStat,
bitswapUnwant,
bitswapWantlist
]
================================================
FILE: packages/ipfs-cli/src/commands/bitswap/stat.js
================================================
import prettyBytes from 'pretty-bytes'
import parseDuration from 'parse-duration'
/**
* @typedef {object} Argv
* @property {import('../../types').Context} Argv.ctx
* @property {boolean} Argv.human
* @property {string} Argv.cidBase
* @property {number} Argv.timeout
*/
/** @type {import('yargs').CommandModule} */
const command = {
command: 'stat',
describe: 'Show some diagnostic information on the bitswap agent',
builder: {
'cid-base': {
describe: 'Number base to display CIDs in. Note: specifying a CID base for v0 CIDs will have no effect',
string: true,
default: 'base58btc'
},
human: {
boolean: true,
default: false
},
timeout: {
string: true,
coerce: parseDuration
}
},
async handler ({ ctx: { ipfs, print }, cidBase, human, timeout }) {
const stats = await ipfs.bitswap.stat({
timeout
})
/** @type {Record} */
const output = {
...stats
}
if (human) {
output.blocksReceived = Number(stats.blocksReceived)
output.blocksSent = Number(stats.blocksSent)
output.dataReceived = prettyBytes(Number(stats.dataReceived)).toUpperCase()
output.dataSent = prettyBytes(Number(stats.dataSent)).toUpperCase()
output.dupBlksReceived = Number(stats.dupBlksReceived)
output.dupDataReceived = prettyBytes(Number(stats.dupDataReceived)).toUpperCase()
output.wantlist = `[${stats.wantlist.length} keys]`
} else {
const base = await ipfs.bases.getBase(cidBase)
const wantlist = stats.wantlist.map(cid => cid.toString(base.encoder))
output.wantlist = `[${wantlist.length} keys]
${wantlist.join('\n ')}`
}
print(`bitswap status
provides buffer: ${output.provideBufLen}
blocks received: ${output.blocksReceived}
blocks sent: ${output.blocksSent}
data received: ${output.dataReceived}
data sent: ${output.dataSent}
dup blocks received: ${output.dupBlksReceived}
dup data received: ${output.dupDataReceived}
wantlist ${output.wantlist}
partners [${stats.peers.length}]`)
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/bitswap/unwant.js
================================================
import parseDuration from 'parse-duration'
import { coerceCID } from '../../utils.js'
/**
* @typedef {object} Argv
* @property {import('../../types').Context} Argv.ctx
* @property {import('multiformats/cid').CID} Argv.key
* @property {string} Argv.cidBase
* @property {number} Argv.timeout
*/
/** @type {import('yargs').CommandModule} */
const command = {
command: 'unwant ',
describe: 'Removes a given block from your wantlist',
builder: {
key: {
alias: 'k',
describe: 'Key to remove from your wantlist',
string: true,
coerce: coerceCID
},
'cid-base': {
describe: 'Number base to display CIDs in. Note: specifying a CID base for v0 CIDs will have no effect',
string: true,
default: 'base58btc'
},
timeout: {
string: true,
coerce: parseDuration
}
},
async handler ({ ctx, key, cidBase, timeout }) {
const { ipfs, print } = ctx
const base = await ipfs.bases.getBase(cidBase)
await ipfs.bitswap.unwant(key, {
timeout
})
print(`Key ${key.toString(base.encoder)} removed from wantlist`)
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/bitswap/wantlist.js
================================================
import parseDuration from 'parse-duration'
import { coercePeerId } from '../../utils.js'
/**
* @typedef {object} Argv
* @property {import('../../types').Context} Argv.ctx
* @property {import('@libp2p/interface-peer-id').PeerId} Argv.peer
* @property {string} Argv.cidBase
* @property {number} Argv.timeout
*/
/** @type {import('yargs').CommandModule} */
const command = {
command: 'wantlist [peer]',
describe: 'Print out all blocks currently on the bitswap wantlist for the local peer',
builder: {
peer: {
alias: 'p',
describe: 'Specify which peer to show wantlist for',
string: true,
coerce: coercePeerId
},
'cid-base': {
describe: 'Number base to display CIDs in. Note: specifying a CID base for v0 CIDs will have no effect',
string: true,
default: 'base58btc'
},
timeout: {
string: true,
coerce: parseDuration
}
},
async handler ({ ctx, peer, cidBase, timeout }) {
const { ipfs, print } = ctx
const base = await ipfs.bases.getBase(cidBase)
/** @type {import('multiformats/cid').CID[]} */
let list
if (peer) {
list = await ipfs.bitswap.wantlistForPeer(peer, {
timeout
})
} else {
list = await ipfs.bitswap.wantlist({
timeout
})
}
list.forEach(cid => print(cid.toString(base.encoder)))
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/bitswap.js
================================================
import { commands } from './bitswap/index.js'
/** @type {import('yargs').CommandModule} */
const command = {
command: 'bitswap ',
describe: 'Interact with the bitswap agent',
builder (yargs) {
commands.forEach(command => {
yargs.command(command)
})
return yargs
},
handler () {
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/block/get.js
================================================
import parseDuration from 'parse-duration'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { coerceCID } from '../../utils.js'
/**
* @typedef {object} Argv
* @property {import('../../types').Context} Argv.ctx
* @property {import('multiformats/cid').CID} Argv.key
* @property {number} Argv.timeout
*/
/** @type {import('yargs').CommandModule} */
const command = {
command: 'get ',
describe: 'Get a raw IPFS block',
builder: {
key: {
string: true,
coerce: coerceCID
},
timeout: {
string: true,
coerce: parseDuration
}
},
async handler ({ ctx, key, timeout }) {
const { ipfs, print } = ctx
const block = await ipfs.block.get(key, {
timeout
})
if (block) {
print(uint8ArrayToString(block), false)
} else {
print('Block was unwanted before it could be remotely retrieved')
}
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/block/index.js
================================================
import blockGet from './get.js'
import blockPut from './put.js'
import blockRm from './rm.js'
import blockStat from './stat.js'
/** @type {import('yargs').CommandModule[]} */
export const commands = [
blockGet,
blockPut,
blockRm,
blockStat
]
================================================
FILE: packages/ipfs-cli/src/commands/block/put.js
================================================
import fs from 'fs'
import concat from 'it-concat'
import parseDuration from 'parse-duration'
/**
* @typedef {object} Argv
* @property {import('../../types').Context} Argv.ctx
* @property {string} Argv.block
* @property {string} Argv.format
* @property {string} Argv.mhtype
* @property {number} Argv.mhlen
* @property {import('multiformats/cid').Version} Argv.version
* @property {boolean} Argv.pin
* @property {string} Argv.cidBase
* @property {number} Argv.timeout
*/
/** @type {import('yargs').CommandModule} */
const command = {
command: 'put [block]',
describe: 'Stores input as an IPFS block',
builder: {
format: {
alias: 'f',
describe: 'cid format for blocks to be created with',
default: 'dag-pb'
},
mhtype: {
describe: 'multihash hash function',
default: 'sha2-256'
},
mhlen: {
describe: 'multihash hash length',
default: undefined
},
version: {
describe: 'cid version',
number: true,
default: 0
},
'cid-base': {
describe: 'Number base to display CIDs in',
string: true,
default: 'base58btc'
},
pin: {
describe: 'Pin this block recursively',
boolean: true,
default: false
},
timeout: {
string: true,
coerce: parseDuration
}
},
async handler ({ ctx: { ipfs, print, getStdin }, block, timeout, format, mhtype, mhlen, version, cidBase, pin }) {
let data
if (block) {
data = fs.readFileSync(block)
} else {
data = (await concat(getStdin(), { type: 'buffer' })).subarray()
}
const cid = await ipfs.block.put(data, {
timeout,
format,
mhtype,
version,
pin
})
const base = await ipfs.bases.getBase(cidBase)
print(cid.toString(base.encoder))
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/block/rm.js
================================================
import parseDuration from 'parse-duration'
import { coerceCIDs } from '../../utils.js'
/**
* @typedef {object} Argv
* @property {import('../../types').Context} Argv.ctx
* @property {import('multiformats/cid').CID[]} Argv.hash
* @property {boolean} Argv.force
* @property {boolean} Argv.quiet
* @property {number} Argv.timeout
*/
/** @type {import('yargs').CommandModule} */
const command = {
command: 'rm ',
describe: 'Remove IPFS block(s)',
builder: {
hash: {
type: 'array',
coerce: coerceCIDs
},
force: {
alias: 'f',
describe: 'Ignore nonexistent blocks',
boolean: true,
default: false
},
quiet: {
alias: 'q',
describe: 'Write minimal output',
boolean: true,
default: false
},
timeout: {
string: true,
coerce: parseDuration
}
},
async handler ({ ctx, hash, force, quiet, timeout }) {
const { ipfs, print } = ctx
let errored = false
for await (const result of ipfs.block.rm(hash, {
force,
quiet,
timeout
})) {
if (result.error) {
errored = true
}
if (!quiet) {
print(result.error ? result.error.message : `removed ${result.cid}`)
}
}
if (errored && !quiet) {
throw new Error('some blocks not removed')
}
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/block/stat.js
================================================
import parseDuration from 'parse-duration'
import { coerceCID } from '../../utils.js'
/**
* @typedef {object} Argv
* @property {import('../../types').Context} Argv.ctx
* @property {import('multiformats/cid').CID} Argv.key
* @property {string} Argv.cidBase
* @property {number} Argv.timeout
*/
/** @type {import('yargs').CommandModule} */
const command = {
command: 'stat ',
describe: 'Print information of a raw IPFS block',
builder: {
key: {
string: true,
coerce: coerceCID
},
'cid-base': {
describe: 'Number base to display CIDs in',
string: true,
default: 'base58btc'
},
timeout: {
string: true,
coerce: parseDuration
}
},
async handler ({ ctx, key, cidBase, timeout }) {
const { ipfs, print } = ctx
const stats = await ipfs.block.stat(key, {
timeout
})
const base = await ipfs.bases.getBase(cidBase)
print('Key: ' + stats.cid.toString(base.encoder))
print('Size: ' + stats.size)
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/block.js
================================================
import { commands } from './block/index.js'
/** @type {import('yargs').CommandModule} */
const command = {
command: 'block ',
describe: 'Manipulate raw IPFS blocks',
builder (yargs) {
commands.forEach(command => {
yargs.command(command)
})
return yargs
},
handler () {
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/bootstrap/add.js
================================================
import parseDuration from 'parse-duration'
import { coerceMultiaddr } from '../../utils.js'
/**
* @typedef {object} Argv
* @property {import('../../types').Context} Argv.ctx
* @property {import('@multiformats/multiaddr').Multiaddr} Argv.peer
* @property {boolean} Argv.default
* @property {number} Argv.timeout
*/
/** @type {import('yargs').CommandModule} */
const command = {
command: 'add []',
describe: 'Add peers to the bootstrap list',
builder: {
peer: {
string: true,
coerce: coerceMultiaddr
},
default: {
describe: 'Add default bootstrap nodes',
boolean: true,
default: false
},
timeout: {
string: true,
coerce: parseDuration
}
},
async handler ({ ctx: { ipfs, print }, peer, default: defaultPeers, timeout }) {
let list
if (peer) {
list = await ipfs.bootstrap.add(peer, {
timeout
})
} else if (defaultPeers) {
list = await ipfs.bootstrap.reset({
timeout
})
} else {
throw new Error('Please specify a peer or the --default flag')
}
list.Peers.forEach((peer) => print(peer.toString()))
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/bootstrap/index.js
================================================
import bootstrapAdd from './add.js'
import bootstrapList from './list.js'
import bootstrapRm from './rm.js'
/** @type {import('yargs').CommandModule[]} */
export const commands = [
bootstrapAdd,
bootstrapList,
bootstrapRm
]
================================================
FILE: packages/ipfs-cli/src/commands/bootstrap/list.js
================================================
import parseDuration from 'parse-duration'
/**
* @typedef {object} Argv
* @property {import('../../types').Context} Argv.ctx
* @property {number} Argv.timeout
*/
/** @type {import('yargs').CommandModule} */
const command = {
command: 'list',
describe: 'Show peers in the bootstrap list',
builder: {
timeout: {
string: true,
coerce: parseDuration
}
},
async handler ({ ctx: { ipfs, print }, timeout }) {
const list = await ipfs.bootstrap.list({
timeout
})
list.Peers.forEach((node) => print(node.toString()))
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/bootstrap/rm.js
================================================
import parseDuration from 'parse-duration'
import { coerceMultiaddr } from '../../utils.js'
/**
* @typedef {object} Argv
* @property {import('../../types').Context} Argv.ctx
* @property {import('@multiformats/multiaddr').Multiaddr} Argv.peer
* @property {boolean} Argv.all
* @property {number} Argv.timeout
*/
/** @type {import('yargs').CommandModule} */
const command = {
command: 'rm []',
describe: 'Removes peers from the bootstrap list',
builder: {
peer: {
string: true,
coerce: coerceMultiaddr
},
all: {
boolean: true,
describe: 'Remove all bootstrap peers',
default: false
},
timeout: {
string: true,
coerce: parseDuration
}
},
async handler ({ ctx: { ipfs, print }, all, peer, timeout }) {
let list
if (peer) {
list = await ipfs.bootstrap.rm(peer, {
timeout
})
} else if (all) {
list = await ipfs.bootstrap.clear({
timeout
})
} else {
throw new Error('Please specify a peer or the --all flag')
}
list.Peers.forEach((peer) => print(peer.toString()))
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/bootstrap.js
================================================
import { commands } from './bootstrap/index.js'
/** @type {import('yargs').CommandModule} */
const command = {
command: 'bootstrap ',
describe: 'Show or edit the list of bootstrap peers',
builder (yargs) {
commands.forEach(command => {
yargs.command(command)
})
return yargs
},
handler () {
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/cat.js
================================================
import parseDuration from 'parse-duration'
/**
* @typedef {object} Argv
* @property {import('../types').Context} Argv.ctx
* @property {string} Argv.ipfsPath
* @property {number} Argv.offset
* @property {number} Argv.length
* @property {boolean} Argv.preload
* @property {number} Argv.timeout
*/
/** @type {import('yargs').CommandModule} */
const command = {
command: 'cat ',
describe: 'Fetch and cat an IPFS path referencing a file',
builder: {
offset: {
alias: 'o',
number: true,
describe: 'Byte offset to begin reading from'
},
length: {
alias: ['n', 'count'],
number: true,
describe: 'Maximum number of bytes to read'
},
preload: {
boolean: true,
default: true,
describe: 'Preload this object when adding'
},
timeout: {
string: true,
coerce: parseDuration
}
},
async handler ({ ctx: { ipfs, print }, ipfsPath, offset, length, preload, timeout }) {
for await (const buf of ipfs.cat(ipfsPath, { offset, length, preload, timeout })) {
print.write(buf)
}
}
}
export default command
================================================
FILE: packages/ipfs-cli/src/commands/cid/base32.js
================================================
import split from 'it-split'
import { CID } from 'multiformats/cid'
import { base32 } from 'multiformats/bases/base32'
import { toString as uint8arrayToString } from 'uint8arrays/to-string'
/**
* @typedef {object} Argv
* @property {import('../../types').Context} Argv.ctx
* @property {string[]} [Argv.cids]
*/
/** @type {import('yargs').CommandModule