From a8bc4d4e361a1289c79907faa8dcd9f5ec452435 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Wed, 9 Jan 2019 13:05:41 -0500 Subject: [PATCH] move non-proto code into extras/, switch to go modules, drop old dht --- .travis.yml | 17 +- Gopkg.lock | 352 ---------- Gopkg.toml | 47 -- Makefile | 10 +- README.md | 69 -- blobex/server.go | 2 +- claim/decode.go | 3 +- cmd/dht.go | 63 -- cmd/franklin.go | 187 ----- cmd/root.go | 26 - cmd/test.go | 31 - dht/.gitignore | 1 - dht/LICENSE | 21 - dht/README.md | 87 --- dht/bitmap.go | 163 ----- dht/bitmap_test.go | 69 -- dht/container.go | 289 -------- dht/container_test.go | 196 ------ dht/dht.go | 228 ------ dht/krpc.go | 655 ------------------ dht/main_test.go | 39 -- dht/routingtable.go | 597 ---------------- dht/util.go | 133 ---- dht/util_test.go | 100 --- {api => extras/api}/server.go | 6 +- {crypto => extras/crypto}/crypto.go | 2 +- {errors => extras/errors}/README.md | 0 {errors => extras/errors}/errors.go | 0 {jsonrpc => extras/jsonrpc}/daemon.go | 2 +- {jsonrpc => extras/jsonrpc}/daemon_test.go | 0 {jsonrpc => extras/jsonrpc}/daemon_types.go | 2 +- {null => extras/null}/LICENSE | 0 {null => extras/null}/README.md | 0 {null => extras/null}/bool.go | 0 {null => extras/null}/bool_test.go | 0 {null => extras/null}/byte.go | 0 {null => extras/null}/byte_test.go | 0 {null => extras/null}/bytes.go | 0 {null => extras/null}/bytes_test.go | 0 {null => extras/null}/convert/convert.go | 0 {null => extras/null}/convert/convert_test.go | 2 +- {null => extras/null}/float32.go | 0 {null => extras/null}/float32_test.go | 0 {null => extras/null}/float64.go | 0 {null => extras/null}/float64_test.go | 0 {null => extras/null}/int.go | 0 {null => extras/null}/int16.go | 0 {null => extras/null}/int16_test.go | 0 {null => extras/null}/int32.go | 0 {null => extras/null}/int32_test.go | 0 {null => extras/null}/int64.go | 0 {null => extras/null}/int64_test.go | 0 {null => extras/null}/int8.go | 0 {null => extras/null}/int8_test.go | 0 {null => extras/null}/int_test.go | 0 {null => extras/null}/json.go | 0 {null => extras/null}/json_test.go | 0 {null => extras/null}/nullable.go | 0 {null => extras/null}/string.go | 0 {null => extras/null}/string_test.go | 0 {null => extras/null}/time.go | 0 {null => extras/null}/time_test.go | 0 {null => extras/null}/uint.go | 0 {null => extras/null}/uint16.go | 0 {null => extras/null}/uint16_test.go | 0 {null => extras/null}/uint32.go | 0 {null => extras/null}/uint32_test.go | 0 {null => extras/null}/uint64.go | 0 {null => extras/null}/uint64_test.go | 0 {null => extras/null}/uint8.go | 0 {null => extras/null}/uint8_test.go | 0 {null => extras/null}/uint_test.go | 0 {query => extras/query}/query.go | 4 +- {stop => extras/stop}/readme.md | 0 {stop => extras/stop}/stop.go | 0 {travis => extras/travis}/travis.go | 2 +- {travis => extras/travis}/webhook.go | 0 {util => extras/util}/slack.go | 2 +- {util => extras/util}/slice.go | 0 {util => extras/util}/underscore.go | 0 {validator => extras/validator}/bool.go | 0 go.mod | 50 ++ go.sum | 131 ++++ img/filewatchers.png | Bin 20983 -> 0 bytes lbrycrd/address.go | 2 +- lbrycrd/client.go | 2 +- main.go | 15 +- readme.md | 34 + stream/blob.go | 2 +- stream/json.go | 2 +- stream/stream.go | 2 +- 91 files changed, 264 insertions(+), 3383 deletions(-) delete mode 100644 Gopkg.lock delete mode 100644 Gopkg.toml delete mode 100644 README.md delete mode 100644 cmd/dht.go delete mode 100644 cmd/franklin.go delete mode 100644 cmd/root.go delete mode 100644 cmd/test.go delete mode 100644 dht/.gitignore delete mode 100644 dht/LICENSE delete mode 100644 dht/README.md delete mode 100644 dht/bitmap.go delete mode 100644 dht/bitmap_test.go delete mode 100644 dht/container.go delete mode 100644 dht/container_test.go delete mode 100644 dht/dht.go delete mode 100644 dht/krpc.go delete mode 100644 dht/main_test.go delete mode 100644 dht/routingtable.go delete mode 100644 dht/util.go delete mode 100644 dht/util_test.go rename {api => extras/api}/server.go (98%) rename {crypto => extras/crypto}/crypto.go (96%) rename {errors => extras/errors}/README.md (100%) rename {errors => extras/errors}/errors.go (100%) rename {jsonrpc => extras/jsonrpc}/daemon.go (99%) rename {jsonrpc => extras/jsonrpc}/daemon_test.go (100%) rename {jsonrpc => extras/jsonrpc}/daemon_types.go (99%) rename {null => extras/null}/LICENSE (100%) rename {null => extras/null}/README.md (100%) rename {null => extras/null}/bool.go (100%) rename {null => extras/null}/bool_test.go (100%) rename {null => extras/null}/byte.go (100%) rename {null => extras/null}/byte_test.go (100%) rename {null => extras/null}/bytes.go (100%) rename {null => extras/null}/bytes_test.go (100%) rename {null => extras/null}/convert/convert.go (100%) rename {null => extras/null}/convert/convert_test.go (99%) rename {null => extras/null}/float32.go (100%) rename {null => extras/null}/float32_test.go (100%) rename {null => extras/null}/float64.go (100%) rename {null => extras/null}/float64_test.go (100%) rename {null => extras/null}/int.go (100%) rename {null => extras/null}/int16.go (100%) rename {null => extras/null}/int16_test.go (100%) rename {null => extras/null}/int32.go (100%) rename {null => extras/null}/int32_test.go (100%) rename {null => extras/null}/int64.go (100%) rename {null => extras/null}/int64_test.go (100%) rename {null => extras/null}/int8.go (100%) rename {null => extras/null}/int8_test.go (100%) rename {null => extras/null}/int_test.go (100%) rename {null => extras/null}/json.go (100%) rename {null => extras/null}/json_test.go (100%) rename {null => extras/null}/nullable.go (100%) rename {null => extras/null}/string.go (100%) rename {null => extras/null}/string_test.go (100%) rename {null => extras/null}/time.go (100%) rename {null => extras/null}/time_test.go (100%) rename {null => extras/null}/uint.go (100%) rename {null => extras/null}/uint16.go (100%) rename {null => extras/null}/uint16_test.go (100%) rename {null => extras/null}/uint32.go (100%) rename {null => extras/null}/uint32_test.go (100%) rename {null => extras/null}/uint64.go (100%) rename {null => extras/null}/uint64_test.go (100%) rename {null => extras/null}/uint8.go (100%) rename {null => extras/null}/uint8_test.go (100%) rename {null => extras/null}/uint_test.go (100%) rename {query => extras/query}/query.go (98%) rename {stop => extras/stop}/readme.md (100%) rename {stop => extras/stop}/stop.go (100%) rename {travis => extras/travis}/travis.go (98%) rename {travis => extras/travis}/webhook.go (100%) rename {util => extras/util}/slack.go (97%) rename {util => extras/util}/slice.go (100%) rename {util => extras/util}/underscore.go (100%) rename {validator => extras/validator}/bool.go (100%) create mode 100644 go.mod create mode 100644 go.sum delete mode 100644 img/filewatchers.png create mode 100644 readme.md diff --git a/.travis.yml b/.travis.yml index 363e427..e48f305 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,18 @@ os: linux -dist: trusty +dist: xenial language: go go: - - "1.10.x" + - 1.11.x + +env: + - GO111MODULE=on + +install: true + +script: + # Fail if a .go file hasn't been formatted with gofmt + - test -z $(gofmt -s -l $(find . -iname '*.go' -type f | grep -v /vendor/ )) + - make + +notifications: + email: false \ No newline at end of file diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index 6059676..0000000 --- a/Gopkg.lock +++ /dev/null @@ -1,352 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - branch = "master" - digest = "1:cc8ebf0c6745d09f728f1fa4fbd29baaa2e3a65efb49b5fefb0c163171ee7863" - name = "github.com/btcsuite/btcd" - packages = [ - "btcec", - "btcjson", - "chaincfg", - "chaincfg/chainhash", - "rpcclient", - "wire", - ] - pruneopts = "" - revision = "86fed781132ac890ee03e906e4ecd5d6fa180c64" - -[[projects]] - branch = "master" - digest = "1:30d4a548e09bca4a0c77317c58e7407e2a65c15325e944f9c08a7b7992f8a59e" - name = "github.com/btcsuite/btclog" - packages = ["."] - pruneopts = "" - revision = "84c8d2346e9fc8c7b947e243b9c24e6df9fd206a" - -[[projects]] - branch = "master" - digest = "1:b0f4d2431c167d7127a029210c1a7cdc33c9114c1b3fd3582347baad5e832588" - name = "github.com/btcsuite/btcutil" - packages = [ - ".", - "base58", - "bech32", - ] - pruneopts = "" - revision = "d4cc87b860166d00d6b5b9e0d3b3d71d6088d4d4" - -[[projects]] - branch = "master" - digest = "1:422f38d57f1bc0fdc34f26d0f1026869a3710400b09b5478c9288efa13573cfa" - name = "github.com/btcsuite/go-socks" - packages = ["socks"] - pruneopts = "" - revision = "4720035b7bfd2a9bb130b1c184f8bbe41b6f0d0f" - -[[projects]] - branch = "master" - digest = "1:dfc248d5e6e1582fdec83796d3d1d451aa6cae773c4e4ba1dac2838caef6d381" - name = "github.com/btcsuite/websocket" - packages = ["."] - pruneopts = "" - revision = "31079b6807923eb23992c421b114992b95131b55" - -[[projects]] - digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b" - name = "github.com/davecgh/go-spew" - packages = ["spew"] - pruneopts = "" - revision = "346938d642f2ec3594ed81d874461961cd0faa76" - version = "v1.1.0" - -[[projects]] - digest = "1:968d8903d598e3fae738325d3410f33f07ea6a2b9ee5591e9c262ee37df6845a" - name = "github.com/go-errors/errors" - packages = ["."] - pruneopts = "" - revision = "a6af135bd4e28680facf08a3d206b454abc877a4" - version = "v1.0.1" - -[[projects]] - branch = "master" - digest = "1:cd5bab9c9e23ffa6858eaa79dc827fd84bc24bc00b0cfb0b14036e393da2b1fa" - name = "github.com/go-ini/ini" - packages = ["."] - pruneopts = "" - revision = "5cf292cae48347c2490ac1a58fe36735fb78df7e" - -[[projects]] - digest = "1:f958a1c137db276e52f0b50efee41a1a389dcdded59a69711f3e872757dab34b" - name = "github.com/golang/protobuf" - packages = [ - "jsonpb", - "proto", - "ptypes", - "ptypes/any", - "ptypes/duration", - "ptypes/struct", - "ptypes/timestamp", - ] - pruneopts = "" - revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" - version = "v1.1.0" - -[[projects]] - digest = "1:64d212c703a2b94054be0ce470303286b177ad260b2f89a307e3d1bb6c073ef6" - name = "github.com/gorilla/websocket" - packages = ["."] - pruneopts = "" - revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" - version = "v1.2.0" - -[[projects]] - digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be" - name = "github.com/inconshreveable/mousetrap" - packages = ["."] - pruneopts = "" - revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" - version = "v1.0" - -[[projects]] - digest = "1:5e30b8342813a6a85a647f9277e34ffcd5872dc57ab590dd9b251b145b6ec88f" - name = "github.com/lbryio/ozzo-validation" - packages = ["."] - pruneopts = "" - revision = "d1008ad1fd04ceb5faedaf34881df0c504382706" - version = "v3.1" - -[[projects]] - branch = "master" - digest = "1:349bf8c6b66272abd25af9538be35071991811b6ba3e5c849515c0bfcf3f2bd0" - name = "github.com/lbryio/types" - packages = ["go"] - pruneopts = "" - revision = "594241d24e0025d769d2cb58168536b6963d51cf" - -[[projects]] - branch = "master" - digest = "1:eb9117392ee8e7aa44f78e0db603f70b1050ee0ebda4bd40040befb5b218c546" - name = "github.com/mitchellh/mapstructure" - packages = ["."] - pruneopts = "" - revision = "bb74f1db0675b241733089d5a1faa5dd8b0ef57b" - -[[projects]] - digest = "1:3cb50c403fa46c85697dbc4e06a95008689e058f33466b7eb8d31ea0eb291ea3" - name = "github.com/nlopes/slack" - packages = ["."] - pruneopts = "" - revision = "8ab4d0b364ef1e9af5d102531da20d5ec902b6c4" - version = "v0.2.0" - -[[projects]] - digest = "1:3962f553b77bf6c03fc07cd687a22dd3b00fe11aa14d31194f5505f5bb65cdc8" - name = "github.com/sergi/go-diff" - packages = ["diffmatchpatch"] - pruneopts = "" - revision = "1744e2970ca51c86172c8190fadad617561ed6e7" - version = "v1.0.0" - -[[projects]] - branch = "master" - digest = "1:67b7dcb3b7e67cb6f96fb38fe7358bc1210453189da210e40cf357a92d57c1c1" - name = "github.com/shopspring/decimal" - packages = ["."] - pruneopts = "" - revision = "19e3cb6c29303990525b56f51acf77c5630dd88a" - -[[projects]] - branch = "master" - digest = "1:c92f01303e3ab3b5da92657841639cb53d1548f0d2733d12ef3b9fd9d47c869e" - name = "github.com/sirupsen/logrus" - packages = ["."] - pruneopts = "" - revision = "ea8897e79973357ba785ac2533559a6297e83c44" - -[[projects]] - branch = "master" - digest = "1:d0b38ba6da419a6d4380700218eeec8623841d44a856bb57369c172fbf692ab4" - name = "github.com/spf13/cast" - packages = ["."] - pruneopts = "" - revision = "8965335b8c7107321228e3e3702cab9832751bac" - -[[projects]] - branch = "master" - digest = "1:bfbf4a9c265ef41f8d03c9d91e340aaddae835710eaed6cd2e6be889cbc05f56" - name = "github.com/spf13/cobra" - packages = ["."] - pruneopts = "" - revision = "1e58aa3361fd650121dceeedc399e7189c05674a" - -[[projects]] - digest = "1:8e243c568f36b09031ec18dff5f7d2769dcf5ca4d624ea511c8e3197dc3d352d" - name = "github.com/spf13/pflag" - packages = ["."] - pruneopts = "" - revision = "583c0c0531f06d5278b7d917446061adc344b5cd" - version = "v1.0.1" - -[[projects]] - branch = "master" - digest = "1:22d3674d44ee93f52a9c0b6a22d1f736a0ad9ac3f9d2c1ca8648f3c9ce9910bd" - name = "github.com/ybbus/jsonrpc" - packages = ["."] - pruneopts = "" - revision = "2a548b7d822dd62717337a6b1e817fae1b14660a" - -[[projects]] - branch = "master" - digest = "1:3610c577942fbfd2c8975d70a2342bbd13f30cf214237fb8f920c9a6cec0f14a" - name = "github.com/zeebo/bencode" - packages = ["."] - pruneopts = "" - revision = "d522839ac797fc43269dae6a04a1f8be475a915d" - -[[projects]] - branch = "master" - digest = "1:8af4dda167d0ef21ab0affc797bff87ed0e87c57bd1d9bf57ad8f72d348c7932" - name = "golang.org/x/crypto" - packages = [ - "ripemd160", - "sha3", - "ssh/terminal", - ] - pruneopts = "" - revision = "8ac0e0d97ce45cd83d1d7243c060cb8461dda5e9" - -[[projects]] - branch = "master" - digest = "1:5dc6753986b9eeba4abdf05dedc5ba06bb52dad43cc8aad35ffb42bb7adfa68f" - name = "golang.org/x/net" - packages = [ - "context", - "http/httpguts", - "http2", - "http2/hpack", - "idna", - "internal/timeseries", - "trace", - ] - pruneopts = "" - revision = "db08ff08e8622530d9ed3a0e8ac279f6d4c02196" - -[[projects]] - branch = "master" - digest = "1:baee54aa41cb93366e76a9c29f8dd2e4c4e6a35ff89551721d5275d2c858edc9" - name = "golang.org/x/sys" - packages = [ - "unix", - "windows", - ] - pruneopts = "" - revision = "bff228c7b664c5fce602223a05fb708fd8654986" - -[[projects]] - digest = "1:5acd3512b047305d49e8763eef7ba423901e85d5dd2fd1e71778a0ea8de10bd4" - name = "golang.org/x/text" - packages = [ - "collate", - "collate/build", - "internal/colltab", - "internal/gen", - "internal/tag", - "internal/triegen", - "internal/ucd", - "language", - "secure/bidirule", - "transform", - "unicode/bidi", - "unicode/cldr", - "unicode/norm", - "unicode/rangetable", - ] - pruneopts = "" - revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" - version = "v0.3.0" - -[[projects]] - branch = "master" - digest = "1:1b3b4ec811695907c4a3cb92e4f32834a4a42459bff7e02068b6b2b5344803cd" - name = "google.golang.org/genproto" - packages = ["googleapis/rpc/status"] - pruneopts = "" - revision = "af9cb2a35e7f169ec875002c1829c9b315cddc04" - -[[projects]] - digest = "1:15656947b87a6a240e61dcfae9e71a55a8d5677f240d12ab48f02cdbabf1e309" - name = "google.golang.org/grpc" - packages = [ - ".", - "balancer", - "balancer/base", - "balancer/roundrobin", - "codes", - "connectivity", - "credentials", - "encoding", - "encoding/proto", - "grpclog", - "internal", - "internal/backoff", - "internal/channelz", - "internal/envconfig", - "internal/grpcrand", - "internal/transport", - "keepalive", - "metadata", - "naming", - "peer", - "resolver", - "resolver/dns", - "resolver/passthrough", - "stats", - "status", - "tap", - ] - pruneopts = "" - revision = "8dea3dc473e90c8179e519d91302d0597c0ca1d1" - version = "v1.15.0" - -[[projects]] - digest = "1:f771bf87a3253de520c2af6fb6e75314dce0fedc0b30b208134fe502932bb15d" - name = "gopkg.in/nullbio/null.v6" - packages = ["convert"] - pruneopts = "" - revision = "40264a2e6b7972d183906cf17663983c23231c82" - version = "v6.3" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - input-imports = [ - "github.com/btcsuite/btcd/chaincfg", - "github.com/btcsuite/btcd/chaincfg/chainhash", - "github.com/btcsuite/btcd/rpcclient", - "github.com/btcsuite/btcutil", - "github.com/btcsuite/btcutil/base58", - "github.com/davecgh/go-spew/spew", - "github.com/go-errors/errors", - "github.com/go-ini/ini", - "github.com/golang/protobuf/jsonpb", - "github.com/golang/protobuf/proto", - "github.com/lbryio/ozzo-validation", - "github.com/lbryio/types/go", - "github.com/mitchellh/mapstructure", - "github.com/nlopes/slack", - "github.com/sergi/go-diff/diffmatchpatch", - "github.com/shopspring/decimal", - "github.com/sirupsen/logrus", - "github.com/spf13/cast", - "github.com/spf13/cobra", - "github.com/ybbus/jsonrpc", - "github.com/zeebo/bencode", - "golang.org/x/crypto/ripemd160", - "golang.org/x/crypto/sha3", - "golang.org/x/net/context", - "google.golang.org/grpc", - "gopkg.in/nullbio/null.v6/convert", - ] - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index f8f4d95..0000000 --- a/Gopkg.toml +++ /dev/null @@ -1,47 +0,0 @@ -[[constraint]] - name = "github.com/davecgh/go-spew" - version = "1.1.0" - -[[constraint]] - name = "github.com/go-errors/errors" - version = "1.0.0" - -[[constraint]] - branch = "master" - name = "github.com/mitchellh/mapstructure" - -[[constraint]] - branch = "master" - name = "github.com/shopspring/decimal" - -[[constraint]] - name = "github.com/sirupsen/logrus" - branch = "master" - -[[constraint]] - name = "github.com/spf13/cast" - branch = "master" - -[[constraint]] - branch = "master" - name = "github.com/spf13/cobra" - -[[constraint]] - branch = "master" - name = "github.com/ybbus/jsonrpc" - -[[constraint]] - branch = "master" - name = "github.com/zeebo/bencode" - -[[constraint]] - branch = "master" - name = "github.com/btcsuite/btcd" - -[[constraint]] - branch = "master" - name = "github.com/go-ini/ini" - -[[constraint]] - branch = "master" - name = "github.com/btcsuite/btcutil" \ No newline at end of file diff --git a/Makefile b/Makefile index 1b9d12a..6a83635 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,17 @@ BINARY=lbry DIR = $(shell cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) -VENDOR_DIR = vendor VERSION=$(shell git --git-dir=${DIR}/.git describe --dirty --always --long --abbrev=7) LDFLAGS = -ldflags "-X main.Version=${VERSION}" -.PHONY: build dep clean +.PHONY: build clean .DEFAULT_GOAL: build -build: dep +build: CGO_ENABLED=0 go build ${LDFLAGS} -asmflags -trimpath=${DIR} -o ${DIR}/${BINARY} main.go -dep: | $(VENDOR_DIR) - -$(VENDOR_DIR): - go get github.com/golang/dep/cmd/dep && dep ensure - clean: if [ -f ${DIR}/${BINARY} ]; then rm ${DIR}/${BINARY}; fi diff --git a/README.md b/README.md deleted file mode 100644 index c315a05..0000000 --- a/README.md +++ /dev/null @@ -1,69 +0,0 @@ -[![Build Status](https://travis-ci.org/lbryio/lbry.go.svg?branch=master)](https://travis-ci.org/lbryio/lbry.go) -# LBRY in Golang - -lbry.go is a set of tools and projects implemented in Golang. See each subfolder for more details - -## Installation - -No installation required for lbry.go - -## Usage - -See individual subfolders for usage instructions - -## Running from Source - -### Go - -Make sure you have Go 1.10.1+ - -- Ubuntu: https://launchpad.net/~longsleep/+archive/ubuntu/golang-backports or https://github.com/golang/go/wiki/Ubuntu -- OSX: `brew install go` - - -### Lbrycrd - -_not strictly necessary, but recommended_ - -- Install lbrycrdd (https://github.com/lbryio/lbrycrd/releases) -- Ensure `~/.lbrycrd/lbrycrd.conf` file exists with username and password. - If you don't have one, run: - - ``` - mkdir -p ~/.lbrycrd - echo -e "rpcuser=lbryrpc\nrpcpassword=$(env LC_CTYPE=C LC_ALL=C tr -dc A-Za-z0-9 < /dev/urandom | head -c 16 | xargs)" > ~/.lbrycrd/lbrycrd.conf - ``` - -- Run `./lbrycrdd -server -daemon -txindex`. If you get an error about indexing, add the `-reindex` flag for one run. You will only need to - reindex once. - -### building lbry.go -clone the repository -``` -go get -u github.com/lbryio/lbry.go -cd "$(go env GOPATH)/src/github.com/lbryio/lbry.go" -``` -run `make` from the root directory to build the binary - -## Contributing - -Contributions to this project are welcome, encouraged, and compensated. For more details, see [lbry.io/faq/contributing](https://lbry.io/faq/contributing) - - GO strictly enforces a correct syntax therefore you might need to run `go fmt` from inside the each working directory. - -When using an IDE like `Goland` you should set up file watchers such as to automatically format your code and sort your imports. - -![alt text](img/filewatchers.png "file watchers") - -## License - -See [LICENSE](LICENSE) - -## Security - -We take security seriously. Please contact security@lbry.io regarding any issues you may encounter. -Our PGP key is [here](https://keybase.io/lbry/key.asc) if you need it. - -## Contact - -The primary contact for this project is [@nikooo777](https://github.com/nikooo777) (niko@lbry.io) \ No newline at end of file diff --git a/blobex/server.go b/blobex/server.go index fac9802..4df3010 100644 --- a/blobex/server.go +++ b/blobex/server.go @@ -4,7 +4,7 @@ import ( "fmt" "net" - "github.com/lbryio/lbry.go/errors" + "github.com/lbryio/lbry.go/extras/errors" "golang.org/x/net/context" "google.golang.org/grpc" diff --git a/claim/decode.go b/claim/decode.go index 86fc103..f6a1362 100644 --- a/claim/decode.go +++ b/claim/decode.go @@ -3,9 +3,10 @@ package claim import ( "bytes" + types "github.com/lbryio/types/go" + "github.com/golang/protobuf/jsonpb" "github.com/golang/protobuf/proto" - types "github.com/lbryio/types/go" ) func ToJSON(value []byte) (string, error) { diff --git a/cmd/dht.go b/cmd/dht.go deleted file mode 100644 index 076ebf0..0000000 --- a/cmd/dht.go +++ /dev/null @@ -1,63 +0,0 @@ -package cmd - -import ( - "strconv" - "time" - - "github.com/lbryio/lbry.go/dht" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -func init() { - d := &cobra.Command{ - Use: "dht ", - Args: cobra.ExactArgs(1), - Short: "Do DHT things", - Run: dhtCmd, - } - RootCmd.AddCommand(d) - - ping := &cobra.Command{ - Use: "ping ", - Args: cobra.ExactArgs(1), - Short: "Ping a node on the DHT", - Run: dhtPingCmd, - } - d.AddCommand(ping) -} - -func dhtCmd(cmd *cobra.Command, args []string) { - log.Errorln("chose a command") -} - -func dhtPingCmd(cmd *cobra.Command, args []string) { - //ip := args[0] - - port := 49449 // + (rand.Int() % 10) - - config := dht.NewStandardConfig() - config.Address = "127.0.0.1:" + strconv.Itoa(port) - config.PrimeNodes = []string{ - "127.0.0.1:10001", - } - - d := dht.New(config) - log.Println("Starting...") - go d.Run() - - time.Sleep(2 * time.Second) - - for { - peers, err := d.FindNode("012b66fc7052d9a0c8cb563b8ede7662003ba65f425c2661b5c6919d445deeb31469be8b842d6faeea3f2b3ebcaec845") - if err != nil { - time.Sleep(time.Second * 1) - continue - } - - log.Println("Found peers:", peers) - break - } - - log.Println("done") -} diff --git a/cmd/franklin.go b/cmd/franklin.go deleted file mode 100644 index 432e2d3..0000000 --- a/cmd/franklin.go +++ /dev/null @@ -1,187 +0,0 @@ -package cmd - -import ( - "strconv" - "sync" - "time" - - "github.com/lbryio/lbry.go/errors" - "github.com/lbryio/lbry.go/jsonrpc" - - "github.com/shopspring/decimal" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -func init() { - var franklinCmd = &cobra.Command{ - Use: "franklin", - Short: "Test availability of homepage content", - Run: func(cmd *cobra.Command, args []string) { - franklin() - }, - } - RootCmd.AddCommand(franklinCmd) -} - -const ( - maxPrice = float64(999) - waitForStart = 5 * time.Second - waitForEnd = 60 * time.Minute - maxParallelTests = 5 -) - -type Result struct { - started bool - finished bool -} - -func franklin() { - conn := jsonrpc.NewClient("") - - var wg sync.WaitGroup - queue := make(chan string) - - var mutex sync.Mutex - results := map[string]Result{} - - for i := 0; i < maxParallelTests; i++ { - go func() { - wg.Add(1) - defer wg.Done() - for { - url, more := <-queue - if !more { - return - } - - res, err := doURL(conn, url) - mutex.Lock() - results[url] = res - mutex.Unlock() - if err != nil { - log.Errorln(url + ": " + err.Error()) - } - } - }() - } - - urls := []string{"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"} - for _, url := range urls { - queue <- url - } - close(queue) - - wg.Wait() - - countStarted := 0 - countFinished := 0 - for _, r := range results { - if r.started { - countStarted++ - } - if r.finished { - countFinished++ - } - } - - log.Println("Started: " + strconv.Itoa(countStarted) + " of " + strconv.Itoa(len(results))) - log.Println("Finished: " + strconv.Itoa(countFinished) + " of " + strconv.Itoa(len(results))) -} - -func doURL(conn *jsonrpc.Client, url string) (Result, error) { - log.Infoln(url + ": Starting") - - result := Result{} - - price, err := conn.StreamCostEstimate(url, nil) - if err != nil { - return result, err - } - - if price == nil { - return result, errors.Err("could not get price of " + url) - } - - if decimal.Decimal(*price).Cmp(decimal.NewFromFloat(maxPrice)) == 1 { - return result, errors.Err("the price of " + url + " is too damn high") - } - - startTime := time.Now() - get, err := conn.Get(url, nil, nil) - if err != nil { - return result, err - } else if get == nil { - return result, errors.Err("received no response for 'get' of " + url) - } - - if get.Completed { - log.Infoln(url + ": cannot test because we already have it") - return result, nil - } - - log.Infoln(url + ": get took " + time.Since(startTime).String()) - - log.Infoln(url + ": waiting " + waitForStart.String() + " to see if it starts") - - time.Sleep(waitForStart) - - fileStartedResult, err := conn.FileList(jsonrpc.FileListOptions{Outpoint: &get.Outpoint}) - if err != nil { - return result, err - } - - if fileStartedResult == nil || len(*fileStartedResult) < 1 { - log.Errorln(url + ": failed to start in " + waitForStart.String()) - } else if (*fileStartedResult)[0].Completed { - log.Infoln(url + ": already finished after " + waitForStart.String() + ". boom!") - result.started = true - result.finished = true - return result, nil - } else if (*fileStartedResult)[0].WrittenBytes == 0 { - log.Errorln(url + ": says it started, but has 0 bytes downloaded after " + waitForStart.String()) - } else { - log.Infoln(url + ": started, with " + strconv.FormatUint((*fileStartedResult)[0].WrittenBytes, 10) + " bytes downloaded") - result.started = true - } - - log.Infoln(url + ": waiting up to " + waitForEnd.String() + " for file to finish") - - var fileFinishedResult *jsonrpc.FileListResponse - ticker := time.NewTicker(15 * time.Second) - // todo: timeout should be based on file size - timeout := time.After(waitForEnd) - -WaitForFinish: - for { - select { - case <-ticker.C: - fileFinishedResult, err = conn.FileList(jsonrpc.FileListOptions{Outpoint: &get.Outpoint}) - if err != nil { - return result, err - } - if fileFinishedResult != nil && len(*fileFinishedResult) > 0 { - if (*fileFinishedResult)[0].Completed { - ticker.Stop() - break WaitForFinish - } else { - log.Infoln(url + ": " + strconv.FormatUint((*fileFinishedResult)[0].WrittenBytes, 10) + " bytes downloaded after " + time.Since(startTime).String()) - } - } - case <-timeout: - ticker.Stop() - break WaitForFinish - } - } - - if fileFinishedResult == nil || len(*fileFinishedResult) < 1 { - log.Errorln(url + ": failed to start at all") - } else if !(*fileFinishedResult)[0].Completed { - log.Errorln(url + ": says it started, but has not finished after " + waitForEnd.String() + " (" + strconv.FormatUint((*fileFinishedResult)[0].WrittenBytes, 10) + " bytes written)") - } else { - log.Infoln(url + ": finished after " + time.Since(startTime).String() + " , with " + strconv.FormatUint((*fileFinishedResult)[0].WrittenBytes, 10) + " bytes downloaded") - result.finished = true - } - - return result, nil -} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index c9eafc2..0000000 --- a/cmd/root.go +++ /dev/null @@ -1,26 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" -) - -// RootCmd represents the base command when called without any subcommands -var RootCmd = &cobra.Command{ - Use: "lbry", - Short: "A command-line swiss army knife for LBRY", - // Uncomment the following line if your bare application - // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { }, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - if err := RootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} diff --git a/cmd/test.go b/cmd/test.go deleted file mode 100644 index 997c2a0..0000000 --- a/cmd/test.go +++ /dev/null @@ -1,31 +0,0 @@ -package cmd - -import ( - "encoding/hex" - "fmt" - - "github.com/lbryio/lbry.go/claim" - - "github.com/spf13/cobra" -) - -func init() { - var testCmd = &cobra.Command{ - Use: "test", - Short: "For testing stuff", - Run: test, - } - RootCmd.AddCommand(testCmd) -} - -func test(cmd *cobra.Command, args []string) { - value, err := hex.DecodeString("080110011ac10108011279080410011a0343617422002a00320d5075626c696320446f6d61696e38004a5568747470733a2f2f737065652e63682f373961653031353766356165356535336232326562646465666663326564623862363130666437372f68773978754137686d326b32645a35477479744a336448372e6a706752005a001a42080110011a30970015f05a30531c465a3f889ab516b972c57c529cd4b57b0bd1685a19c0caa8073f6d4f3db338c1034481fb3eb37241220a696d6167652f6a7065672a5c080110031a4088d15f554d64776f3b43bc63b50c16a69162eb256c9e7afe04505f88a36d7455069de25244834f6d14479b45064d4766fa359bd041886b612040c9dbc9d1d0ec221412bcd69bf6a7d503002f09d34c76f904253a4be2") - if err != nil { - panic(err) - } - s, err := claim.ToJSON(value) - if err != nil { - panic(err) - } - fmt.Println(s) -} diff --git a/dht/.gitignore b/dht/.gitignore deleted file mode 100644 index e43b0f9..0000000 --- a/dht/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.DS_Store diff --git a/dht/LICENSE b/dht/LICENSE deleted file mode 100644 index ab4304b..0000000 --- a/dht/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Dean Karn - -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. diff --git a/dht/README.md b/dht/README.md deleted file mode 100644 index a50e150..0000000 --- a/dht/README.md +++ /dev/null @@ -1,87 +0,0 @@ -![](https://raw.githubusercontent.com/shiyanhui/dht/master/doc/screen-shot.png) - -See the video on the [Youtube](https://www.youtube.com/watch?v=AIpeQtw22kc). - -[中文版README](https://github.com/shiyanhui/dht/blob/master/README_CN.md) - -## Introduction - -DHT implements the bittorrent DHT protocol in Go. Now it includes: - -- [BEP-3 (part)](http://www.bittorrent.org/beps/bep_0003.html) -- [BEP-5](http://www.bittorrent.org/beps/bep_0005.html) -- [BEP-9](http://www.bittorrent.org/beps/bep_0009.html) -- [BEP-10](http://www.bittorrent.org/beps/bep_0010.html) - -It contains two modes, the standard mode and the crawling mode. The standard -mode follows the BEPs, and you can use it as a standard dht server. The crawling -mode aims to crawl as more metadata info as possiple. It doesn't follow the -standard BEPs protocol. With the crawling mode, you can build another [BTDigg](http://btdigg.org/). - -[bthub.io](http://bthub.io) is a BT search engine based on the crawling mode. - -## Installation - - go get github.com/shiyanhui/dht - -## Example - -Below is a simple spider. You can move [here](https://github.com/shiyanhui/dht/blob/master/sample) -to see more samples. - -```go -import ( - "fmt" - "github.com/shiyanhui/dht" -) - -func main() { - downloader := dht.NewWire(65535) - go func() { - // once we got the request result - for resp := range downloader.Response() { - fmt.Println(resp.InfoHash, resp.MetadataInfo) - } - }() - go downloader.Run() - - config := dht.NewCrawlConfig() - config.OnAnnouncePeer = func(infoHash, ip string, port int) { - // request to download the metadata info - downloader.Request([]byte(infoHash), ip, port) - } - d := dht.New(config) - - d.Run() -} -``` - -## Download - -You can download the demo compiled binary file [here](https://github.com/shiyanhui/dht/files/407021/spider.zip). - -## Note - -- The default crawl mode configure costs about 300M RAM. Set **MaxNodes** - and **BlackListMaxSize** to fit yourself. -- Now it cant't run in LAN because of NAT. - -## TODO - -- [ ] NAT Traversal. -- [ ] Implements the full BEP-3. -- [ ] Optimization. - -## FAQ - -#### Why it is slow compared to other spiders ? - -Well, maybe there are several reasons. - -- DHT aims to implements the standard BitTorrent DHT protocol, not born for crawling the DHT network. -- NAT Traversal issue. You run the crawler in a local network. -- It will block ip which looks like bad and a good ip may be mis-judged. - -## License - -MIT, read more [here](https://github.com/shiyanhui/dht/blob/master/LICENSE) diff --git a/dht/bitmap.go b/dht/bitmap.go deleted file mode 100644 index 0df61a1..0000000 --- a/dht/bitmap.go +++ /dev/null @@ -1,163 +0,0 @@ -package dht - -import ( - "fmt" - "strings" -) - -// bitmap represents a bit array. -type bitmap struct { - Size int - data []byte -} - -// newBitmap returns a size-length bitmap pointer. -func newBitmap(size int) *bitmap { - div, mod := size/8, size%8 - if mod > 0 { - div++ - } - return &bitmap{size, make([]byte, div)} -} - -// newBitmapFrom returns a new copyed bitmap pointer which -// newBitmap.data = other.data[:size]. -func newBitmapFrom(other *bitmap, size int) *bitmap { - bitmap := newBitmap(size) - - if size > other.Size { - size = other.Size - } - - div := size / 8 - - for i := 0; i < div; i++ { - bitmap.data[i] = other.data[i] - } - - for i := div * 8; i < size; i++ { - if other.Bit(i) == 1 { - bitmap.Set(i) - } - } - - return bitmap -} - -// newBitmapFromBytes returns a bitmap pointer created from a byte array. -func newBitmapFromBytes(data []byte) *bitmap { - bitmap := newBitmap(len(data) * 8) - copy(bitmap.data, data) - return bitmap -} - -// newBitmapFromString returns a bitmap pointer created from a string. -func newBitmapFromString(data string) *bitmap { - return newBitmapFromBytes([]byte(data)) -} - -// Bit returns the bit at index. -func (bitmap *bitmap) Bit(index int) int { - if index >= bitmap.Size { - panic("index out of range") - } - - div, mod := index/8, index%8 - return int((uint(bitmap.data[div]) & (1 << uint(7-mod))) >> uint(7-mod)) -} - -// set sets the bit at index `index`. If bit is true, set 1, otherwise set 0. -func (bitmap *bitmap) set(index int, bit int) { - if index >= bitmap.Size { - panic("index out of range") - } - - div, mod := index/8, index%8 - shift := byte(1 << uint(7-mod)) - - bitmap.data[div] &= ^shift - if bit > 0 { - bitmap.data[div] |= shift - } -} - -// Set sets the bit at idnex to 1. -func (bitmap *bitmap) Set(index int) { - bitmap.set(index, 1) -} - -// Unset sets the bit at idnex to 0. -func (bitmap *bitmap) Unset(index int) { - bitmap.set(index, 0) -} - -// Compare compares the prefixLen-prefix of two bitmap. -// - If bitmap.data[:prefixLen] < other.data[:prefixLen], return -1. -// - If bitmap.data[:prefixLen] > other.data[:prefixLen], return 1. -// - Otherwise return 0. -func (bitmap *bitmap) Compare(other *bitmap, prefixLen int) int { - if prefixLen > bitmap.Size || prefixLen > other.Size { - panic("index out of range") - } - - div, mod := prefixLen/8, prefixLen%8 - for i := 0; i < div; i++ { - if bitmap.data[i] > other.data[i] { - return 1 - } else if bitmap.data[i] < other.data[i] { - return -1 - } - } - - for i := div * 8; i < div*8+mod; i++ { - bit1, bit2 := bitmap.Bit(i), other.Bit(i) - if bit1 > bit2 { - return 1 - } else if bit1 < bit2 { - return -1 - } - } - - return 0 -} - -// Xor returns the xor value of two bitmap. -func (bitmap *bitmap) Xor(other *bitmap) *bitmap { - if bitmap.Size != other.Size { - panic("size not the same") - } - - distance := newBitmap(bitmap.Size) - div, mod := distance.Size/8, distance.Size%8 - - for i := 0; i < div; i++ { - distance.data[i] = bitmap.data[i] ^ other.data[i] - } - - for i := div * 8; i < div*8+mod; i++ { - distance.set(i, bitmap.Bit(i)^other.Bit(i)) - } - - return distance -} - -// String returns the bit sequence string of the bitmap. -func (bitmap *bitmap) String() string { - div, mod := bitmap.Size/8, bitmap.Size%8 - buff := make([]string, div+mod) - - for i := 0; i < div; i++ { - buff[i] = fmt.Sprintf("%08b", bitmap.data[i]) - } - - for i := div; i < div+mod; i++ { - buff[i] = fmt.Sprintf("%1b", bitmap.Bit(div*8+(i-div))) - } - - return strings.Join(buff, "") -} - -// RawString returns the string value of bitmap.data. -func (bitmap *bitmap) RawString() string { - return string(bitmap.data) -} diff --git a/dht/bitmap_test.go b/dht/bitmap_test.go deleted file mode 100644 index e82f118..0000000 --- a/dht/bitmap_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package dht - -import ( - "testing" -) - -func TestBitmap(t *testing.T) { - a := newBitmap(10) - b := newBitmapFrom(a, 10) - c := newBitmapFromBytes([]byte{48, 49, 50, 51, 52, 53, 54, 55, 56, 57}) - d := newBitmapFromString("0123456789") - e := newBitmap(10) - - // Bit - for i := 0; i < a.Size; i++ { - if a.Bit(i) != 0 { - t.Fail() - } - } - - // Compare - if c.Compare(d, d.Size) != 0 { - t.Fail() - } - - // RawString - if c.RawString() != d.RawString() || c.RawString() != "0123456789" { - t.Fail() - } - - // Set - b.Set(5) - if b.Bit(5) != 1 { - t.Fail() - } - - // Unset - b.Unset(5) - if b.Bit(5) == 1 { - t.Fail() - } - - // String - if e.String() != "0000000000" { - t.Fail() - } - e.Set(9) - if e.String() != "0000000001" { - t.Fail() - } - e.Set(2) - if e.String() != "0010000001" { - t.Fail() - } - - a.Set(0) - a.Set(5) - a.Set(8) - if a.String() != "1000010010" { - t.Fail() - } - - // Xor - b.Set(5) - b.Set(9) - if a.Xor(b).String() != "1000000011" { - t.Fail() - } -} diff --git a/dht/container.go b/dht/container.go deleted file mode 100644 index 17a5321..0000000 --- a/dht/container.go +++ /dev/null @@ -1,289 +0,0 @@ -package dht - -import ( - "container/list" - "sync" -) - -type mapItem struct { - key interface{} - val interface{} -} - -// syncedMap represents a goroutine-safe map. -type syncedMap struct { - *sync.RWMutex - data map[interface{}]interface{} -} - -// newSyncedMap returns a syncedMap pointer. -func newSyncedMap() *syncedMap { - return &syncedMap{ - RWMutex: &sync.RWMutex{}, - data: make(map[interface{}]interface{}), - } -} - -// Get returns the value mapped to key. -func (smap *syncedMap) Get(key interface{}) (val interface{}, ok bool) { - smap.RLock() - defer smap.RUnlock() - - val, ok = smap.data[key] - return -} - -// Has returns whether the syncedMap contains the key. -func (smap *syncedMap) Has(key interface{}) bool { - _, ok := smap.Get(key) - return ok -} - -// Set sets pair {key: val}. -func (smap *syncedMap) Set(key interface{}, val interface{}) { - smap.Lock() - defer smap.Unlock() - - smap.data[key] = val -} - -// Delete deletes the key in the map. -func (smap *syncedMap) Delete(key interface{}) { - smap.Lock() - defer smap.Unlock() - - delete(smap.data, key) -} - -// DeleteMulti deletes keys in batch. -func (smap *syncedMap) DeleteMulti(keys []interface{}) { - smap.Lock() - defer smap.Unlock() - - for _, key := range keys { - delete(smap.data, key) - } -} - -// Clear resets the data. -func (smap *syncedMap) Clear() { - smap.Lock() - defer smap.Unlock() - - smap.data = make(map[interface{}]interface{}) -} - -// Iter returns a chan which output all items. -func (smap *syncedMap) Iter() <-chan mapItem { - ch := make(chan mapItem) - go func() { - smap.RLock() - for key, val := range smap.data { - ch <- mapItem{ - key: key, - val: val, - } - } - smap.RUnlock() - close(ch) - }() - return ch -} - -// Len returns the length of syncedMap. -func (smap *syncedMap) Len() int { - smap.RLock() - defer smap.RUnlock() - - return len(smap.data) -} - -// syncedList represents a goroutine-safe list. -type syncedList struct { - *sync.RWMutex - queue *list.List -} - -// newSyncedList returns a syncedList pointer. -func newSyncedList() *syncedList { - return &syncedList{ - RWMutex: &sync.RWMutex{}, - queue: list.New(), - } -} - -// Front returns the first element of slist. -func (slist *syncedList) Front() *list.Element { - slist.RLock() - defer slist.RUnlock() - - return slist.queue.Front() -} - -// Back returns the last element of slist. -func (slist *syncedList) Back() *list.Element { - slist.RLock() - defer slist.RUnlock() - - return slist.queue.Back() -} - -// PushFront pushs an element to the head of slist. -func (slist *syncedList) PushFront(v interface{}) *list.Element { - slist.Lock() - defer slist.Unlock() - - return slist.queue.PushFront(v) -} - -// PushBack pushs an element to the tail of slist. -func (slist *syncedList) PushBack(v interface{}) *list.Element { - slist.Lock() - defer slist.Unlock() - - return slist.queue.PushBack(v) -} - -// InsertBefore inserts v before mark. -func (slist *syncedList) InsertBefore( - v interface{}, mark *list.Element) *list.Element { - - slist.Lock() - defer slist.Unlock() - - return slist.queue.InsertBefore(v, mark) -} - -// InsertAfter inserts v after mark. -func (slist *syncedList) InsertAfter( - v interface{}, mark *list.Element) *list.Element { - - slist.Lock() - defer slist.Unlock() - - return slist.queue.InsertAfter(v, mark) -} - -// Remove removes e from the slist. -func (slist *syncedList) Remove(e *list.Element) interface{} { - slist.Lock() - defer slist.Unlock() - - return slist.queue.Remove(e) -} - -// Clear resets the list queue. -func (slist *syncedList) Clear() { - slist.Lock() - defer slist.Unlock() - - slist.queue.Init() -} - -// Len returns length of the slist. -func (slist *syncedList) Len() int { - slist.RLock() - defer slist.RUnlock() - - return slist.queue.Len() -} - -// Iter returns a chan which output all elements. -func (slist *syncedList) Iter() <-chan *list.Element { - ch := make(chan *list.Element) - go func() { - slist.RLock() - for e := slist.queue.Front(); e != nil; e = e.Next() { - ch <- e - } - slist.RUnlock() - close(ch) - }() - return ch -} - -// KeyedDeque represents a keyed deque. -type keyedDeque struct { - *sync.RWMutex - *syncedList - index map[interface{}]*list.Element - invertedIndex map[*list.Element]interface{} -} - -// newKeyedDeque returns a newKeyedDeque pointer. -func newKeyedDeque() *keyedDeque { - return &keyedDeque{ - RWMutex: &sync.RWMutex{}, - syncedList: newSyncedList(), - index: make(map[interface{}]*list.Element), - invertedIndex: make(map[*list.Element]interface{}), - } -} - -// Push pushs a keyed-value to the end of deque. -func (deque *keyedDeque) Push(key interface{}, val interface{}) { - deque.Lock() - defer deque.Unlock() - - if e, ok := deque.index[key]; ok { - deque.syncedList.Remove(e) - } - deque.index[key] = deque.syncedList.PushBack(val) - deque.invertedIndex[deque.index[key]] = key -} - -// Get returns the keyed value. -func (deque *keyedDeque) Get(key interface{}) (*list.Element, bool) { - deque.RLock() - defer deque.RUnlock() - - v, ok := deque.index[key] - return v, ok -} - -// Has returns whether key already exists. -func (deque *keyedDeque) HasKey(key interface{}) bool { - _, ok := deque.Get(key) - return ok -} - -// Delete deletes a value named key. -func (deque *keyedDeque) Delete(key interface{}) (v interface{}) { - deque.RLock() - e, ok := deque.index[key] - deque.RUnlock() - - deque.Lock() - defer deque.Unlock() - - if ok { - v = deque.syncedList.Remove(e) - delete(deque.index, key) - delete(deque.invertedIndex, e) - } - - return -} - -// Removes overwrites list.List.Remove. -func (deque *keyedDeque) Remove(e *list.Element) (v interface{}) { - deque.RLock() - key, ok := deque.invertedIndex[e] - deque.RUnlock() - - if ok { - v = deque.Delete(key) - } - - return -} - -// Clear resets the deque. -func (deque *keyedDeque) Clear() { - deque.Lock() - defer deque.Unlock() - - deque.syncedList.Clear() - deque.index = make(map[interface{}]*list.Element) - deque.invertedIndex = make(map[*list.Element]interface{}) -} diff --git a/dht/container_test.go b/dht/container_test.go deleted file mode 100644 index bc359f8..0000000 --- a/dht/container_test.go +++ /dev/null @@ -1,196 +0,0 @@ -package dht - -import ( - "sync" - "testing" -) - -func TestSyncedMap(t *testing.T) { - cases := []mapItem{ - {"a", 0}, - {"b", 1}, - {"c", 2}, - } - - sm := newSyncedMap() - - set := func() { - group := sync.WaitGroup{} - for _, item := range cases { - group.Add(1) - go func(item mapItem) { - sm.Set(item.key, item.val) - group.Done() - }(item) - } - group.Wait() - } - - isEmpty := func() { - if sm.Len() != 0 { - t.Fail() - } - } - - // Set - set() - if sm.Len() != len(cases) { - t.Fail() - } - -Loop: - // Iter - for item := range sm.Iter() { - for _, c := range cases { - if item.key == c.key && item.val == c.val { - continue Loop - } - } - t.Fail() - } - - // Get, Delete, Has - for _, item := range cases { - val, ok := sm.Get(item.key) - if !ok || val != item.val { - t.Fail() - } - - sm.Delete(item.key) - if sm.Has(item.key) { - t.Fail() - } - } - isEmpty() - - // DeleteMulti - set() - sm.DeleteMulti([]interface{}{"a", "b", "c"}) - isEmpty() - - // Clear - set() - sm.Clear() - isEmpty() -} - -func TestSyncedList(t *testing.T) { - sl := newSyncedList() - - insert := func() { - for i := 0; i < 10; i++ { - sl.PushBack(i) - } - } - - isEmpty := func() { - if sl.Len() != 0 { - t.Fail() - } - } - - // PushBack - insert() - - // Len - if sl.Len() != 10 { - t.Fail() - } - - // Iter - i := 0 - for item := range sl.Iter() { - if item.Value.(int) != i { - t.Fail() - } - i++ - } - - // Front - if sl.Front().Value.(int) != 0 { - t.Fail() - } - - // Back - if sl.Back().Value.(int) != 9 { - t.Fail() - } - - // Remove - for i := 0; i < 10; i++ { - if sl.Remove(sl.Front()).(int) != i { - t.Fail() - } - } - isEmpty() - - // Clear - insert() - sl.Clear() - isEmpty() -} - -func TestKeyedDeque(t *testing.T) { - cases := []mapItem{ - {"a", 0}, - {"b", 1}, - {"c", 2}, - } - - deque := newKeyedDeque() - - insert := func() { - for _, item := range cases { - deque.Push(item.key, item.val) - } - } - - isEmpty := func() { - if deque.Len() != 0 { - t.Fail() - } - } - - // Push - insert() - - // Len - if deque.Len() != 3 { - t.Fail() - } - - // Iter - i := 0 - for e := range deque.Iter() { - if e.Value.(int) != i { - t.Fail() - } - i++ - } - - // HasKey, Get, Delete - for _, item := range cases { - if !deque.HasKey(item.key) { - t.Fail() - } - - e, ok := deque.Get(item.key) - if !ok || e.Value.(int) != item.val { - t.Fail() - } - - if deque.Delete(item.key) != item.val { - t.Fail() - } - - if deque.HasKey(item.key) { - t.Fail() - } - } - isEmpty() - - // Clear - insert() - deque.Clear() - isEmpty() -} diff --git a/dht/dht.go b/dht/dht.go deleted file mode 100644 index 3e275ee..0000000 --- a/dht/dht.go +++ /dev/null @@ -1,228 +0,0 @@ -// Package dht implements the bittorrent dht protocol. For more information -// see http://www.bittorrent.org/beps/bep_0005.html. -package dht - -import ( - "encoding/hex" - "errors" - log "github.com/sirupsen/logrus" - "math" - "net" - "time" -) - -// Config represents the configure of dht. -type Config struct { - // in mainline dht, k = 8 - K int - // candidates are udp, udp4, udp6 - Network string - // format is `ip:port` - Address string - // the prime nodes through which we can join in dht network - PrimeNodes []string - // the kbucket expired duration - KBucketExpiredAfter time.Duration - // the node expired duration - NodeExpriedAfter time.Duration - // how long it checks whether the bucket is expired - CheckKBucketPeriod time.Duration - // peer token expired duration - TokenExpiredAfter time.Duration - // the max transaction id - MaxTransactionCursor uint64 - // how many nodes routing table can hold - MaxNodes int - // callback when got get_peers request - OnGetPeers func(string, string, int) - // callback when got announce_peer request - OnAnnouncePeer func(string, string, int) - // the times it tries when send fails - Try int - // the size of packet need to be dealt with - PacketJobLimit int - // the size of packet handler - PacketWorkerLimit int - // the nodes num to be fresh in a kbucket - RefreshNodeNum int -} - -// NewStandardConfig returns a Config pointer with default values. -func NewStandardConfig() *Config { - return &Config{ - K: 8, - Network: "udp4", - Address: ":4444", - PrimeNodes: []string{ - "lbrynet1.lbry.io:4444", - "lbrynet2.lbry.io:4444", - "lbrynet3.lbry.io:4444", - }, - NodeExpriedAfter: time.Duration(time.Minute * 15), - KBucketExpiredAfter: time.Duration(time.Minute * 15), - CheckKBucketPeriod: time.Duration(time.Second * 30), - TokenExpiredAfter: time.Duration(time.Minute * 10), - MaxTransactionCursor: math.MaxUint32, - MaxNodes: 5000, - Try: 2, - PacketJobLimit: 1024, - PacketWorkerLimit: 256, - RefreshNodeNum: 8, - } -} - -// DHT represents a DHT node. -type DHT struct { - *Config - node *node - conn *net.UDPConn - routingTable *routingTable - transactionManager *transactionManager - peersManager *peersManager - tokenManager *tokenManager - Ready bool - packets chan packet - workerTokens chan struct{} -} - -// New returns a DHT pointer. If config is nil, then config will be set to -// the default config. -func New(config *Config) *DHT { - if config == nil { - config = NewStandardConfig() - } - - node, err := newNode(randomString(nodeIDLength), config.Network, config.Address) - if err != nil { - panic(err) - } - - d := &DHT{ - Config: config, - node: node, - packets: make(chan packet, config.PacketJobLimit), - workerTokens: make(chan struct{}, config.PacketWorkerLimit), - } - - return d -} - -// init initializes global variables. -func (dht *DHT) init() { - log.Info("Initializing DHT on " + dht.Address) - log.Infof("Node ID is %s", dht.node.HexID()) - listener, err := net.ListenPacket(dht.Network, dht.Address) - if err != nil { - panic(err) - } - - dht.conn = listener.(*net.UDPConn) - dht.routingTable = newRoutingTable(dht.K, dht) - dht.peersManager = newPeersManager(dht) - dht.tokenManager = newTokenManager(dht.TokenExpiredAfter, dht) - dht.transactionManager = newTransactionManager(dht.MaxTransactionCursor, dht) - - go dht.transactionManager.run() - go dht.tokenManager.clear() -} - -// join makes current node join the dht network. -func (dht *DHT) join() { - for _, addr := range dht.PrimeNodes { - raddr, err := net.ResolveUDPAddr(dht.Network, addr) - if err != nil { - continue - } - - // NOTE: Temporary node has NO node id. - dht.transactionManager.findNode( - &node{addr: raddr}, - dht.node.id.RawString(), - ) - } -} - -// listen receives message from udp. -func (dht *DHT) listen() { - go func() { - buff := make([]byte, 8192) - for { - n, raddr, err := dht.conn.ReadFromUDP(buff) - if err != nil { - continue - } - - dht.packets <- packet{buff[:n], raddr} - } - }() -} - -// FindNode returns peers who have announced having key. -func (dht *DHT) FindNode(key string) ([]*Peer, error) { - if !dht.Ready { - return nil, errors.New("dht not ready") - } - - if len(key) == nodeIDLength*2 { - data, err := hex.DecodeString(key) - if err != nil { - return nil, err - } - key = string(data) - } - - peers := dht.peersManager.GetPeers(key, dht.K) - if len(peers) != 0 { - return peers, nil - } - - ch := make(chan struct{}) - - go func() { - neighbors := dht.routingTable.GetNeighbors(newBitmapFromString(key), dht.K) - - for _, no := range neighbors { - dht.transactionManager.findNode(no, key) - } - - i := 0 - for range time.Tick(time.Second * 1) { - i++ - peers = dht.peersManager.GetPeers(key, dht.K) - if len(peers) != 0 || i >= 30 { - break - } - } - - ch <- struct{}{} - }() - - <-ch - return peers, nil -} - -// Run starts the dht. -func (dht *DHT) Run() { - dht.init() - dht.listen() - dht.join() - - dht.Ready = true - log.Info("DHT ready") - - var pkt packet - tick := time.Tick(dht.CheckKBucketPeriod) - - for { - select { - case pkt = <-dht.packets: - handle(dht, pkt) - case <-tick: - if dht.routingTable.Len() == 0 { - dht.join() - } else if dht.transactionManager.len() == 0 { - go dht.routingTable.Fresh() - } - } - } -} diff --git a/dht/krpc.go b/dht/krpc.go deleted file mode 100644 index bbc4be1..0000000 --- a/dht/krpc.go +++ /dev/null @@ -1,655 +0,0 @@ -package dht - -import ( - "fmt" - "github.com/davecgh/go-spew/spew" - log "github.com/sirupsen/logrus" - "github.com/spf13/cast" - "github.com/zeebo/bencode" - "net" - "reflect" - "strings" - "sync" - "time" -) - -const ( - pingMethod = "ping" - storeMethod = "store" - findNodeMethod = "findNode" - findValueMethod = "findValue" -) - -const ( - generalError = 201 + iota - serverError - protocolError - unknownError -) - -const ( - requestType = 0 - responseType = 1 - errorType = 2 -) - -const ( - // these are strings because bencode requires bytestring keys - headerTypeField = "0" - headerMessageIDField = "1" - headerNodeIDField = "2" - headerPayloadField = "3" - headerArgsField = "4" -) - -type Message interface { - GetID() string - Encode() ([]byte, error) -} - -type Request struct { - ID string - NodeID string - Method string - Args []string -} - -func (r Request) GetID() string { return r.ID } -func (r Request) Encode() ([]byte, error) { - return bencode.EncodeBytes(map[string]interface{}{ - headerTypeField: requestType, - headerMessageIDField: r.ID, - headerNodeIDField: r.NodeID, - headerPayloadField: r.Method, - headerArgsField: r.Args, - }) -} - -type findNodeDatum struct { - ID string - IP string - Port int -} -type Response struct { - ID string - NodeID string - Data string - FindNodeData []findNodeDatum -} - -func (r Response) GetID() string { return r.ID } -func (r Response) Encode() ([]byte, error) { - data := map[string]interface{}{ - headerTypeField: responseType, - headerMessageIDField: r.ID, - headerNodeIDField: r.NodeID, - } - if r.Data != "" { - data[headerPayloadField] = r.Data - } else { - var nodes []interface{} - for _, n := range r.FindNodeData { - nodes = append(nodes, []interface{}{n.ID, n.IP, n.Port}) - } - data[headerPayloadField] = nodes - } - - log.Info("Response data is ") - spew.Dump(data) - return bencode.EncodeBytes(data) -} - -type Error struct { - ID string - NodeID string - Response []string - ExceptionType string -} - -func (e Error) GetID() string { return e.ID } -func (e Error) Encode() ([]byte, error) { - return bencode.EncodeBytes(map[string]interface{}{ - headerTypeField: errorType, - headerMessageIDField: e.ID, - headerNodeIDField: e.NodeID, - headerPayloadField: e.ExceptionType, - headerArgsField: e.Response, - }) -} - -// packet represents the information receive from udp. -type packet struct { - data []byte - raddr *net.UDPAddr -} - -// token represents the token when response getPeers request. -type token struct { - data string - createTime time.Time -} - -// tokenManager managers the tokens. -type tokenManager struct { - *syncedMap - expiredAfter time.Duration - dht *DHT -} - -// newTokenManager returns a new tokenManager. -func newTokenManager(expiredAfter time.Duration, dht *DHT) *tokenManager { - return &tokenManager{ - syncedMap: newSyncedMap(), - expiredAfter: expiredAfter, - dht: dht, - } -} - -// token returns a token. If it doesn't exist or is expired, it will add a -// new token. -func (tm *tokenManager) token(addr *net.UDPAddr) string { - v, ok := tm.Get(addr.IP.String()) - tk, _ := v.(token) - - if !ok || time.Now().Sub(tk.createTime) > tm.expiredAfter { - tk = token{ - data: randomString(5), - createTime: time.Now(), - } - - tm.Set(addr.IP.String(), tk) - } - - return tk.data -} - -// clear removes expired tokens. -func (tm *tokenManager) clear() { - for range time.Tick(time.Minute * 3) { - keys := make([]interface{}, 0, 100) - - for item := range tm.Iter() { - if time.Now().Sub(item.val.(token).createTime) > tm.expiredAfter { - keys = append(keys, item.key) - } - } - - tm.DeleteMulti(keys) - } -} - -// check returns whether the token is valid. -func (tm *tokenManager) check(addr *net.UDPAddr, tokenString string) bool { - key := addr.IP.String() - v, ok := tm.Get(key) - tk, _ := v.(token) - - if ok { - tm.Delete(key) - } - - return ok && tokenString == tk.data -} - -// send sends data to the udp. -func send(dht *DHT, addr *net.UDPAddr, data Message) error { - log.Infof("Sending %s", spew.Sdump(data)) - encoded, err := data.Encode() - if err != nil { - return err - } - log.Infof("Encoded: %s", string(encoded)) - - dht.conn.SetWriteDeadline(time.Now().Add(time.Second * 15)) - - _, err = dht.conn.WriteToUDP(encoded, addr) - return err -} - -// query represents the query data included queried node and query-formed data. -type query struct { - node *node - request Request -} - -// transaction implements transaction. -type transaction struct { - *query - id string - response chan struct{} -} - -// transactionManager represents the manager of transactions. -type transactionManager struct { - *sync.RWMutex - transactions *syncedMap - index *syncedMap - cursor uint64 - maxCursor uint64 - queryChan chan *query - dht *DHT -} - -// newTransactionManager returns new transactionManager pointer. -func newTransactionManager(maxCursor uint64, dht *DHT) *transactionManager { - return &transactionManager{ - RWMutex: &sync.RWMutex{}, - transactions: newSyncedMap(), - index: newSyncedMap(), - maxCursor: maxCursor, - queryChan: make(chan *query, 1024), - dht: dht, - } -} - -// genTransID generates a transaction id and returns it. -func (tm *transactionManager) genTransID() string { - tm.Lock() - defer tm.Unlock() - - tm.cursor = (tm.cursor + 1) % tm.maxCursor - return string(int2bytes(tm.cursor)) -} - -// newTransaction creates a new transaction. -func (tm *transactionManager) newTransaction(id string, q *query) *transaction { - return &transaction{ - id: id, - query: q, - response: make(chan struct{}, tm.dht.Try+1), - } -} - -// genIndexKey generates an indexed key which consists of queryType and -// address. -func (tm *transactionManager) genIndexKey(queryType, address string) string { - return strings.Join([]string{queryType, address}, ":") -} - -// genIndexKeyByTrans generates an indexed key by a transaction. -func (tm *transactionManager) genIndexKeyByTrans(trans *transaction) string { - return tm.genIndexKey(trans.request.Method, trans.node.addr.String()) -} - -// insert adds a transaction to transactionManager. -func (tm *transactionManager) insert(trans *transaction) { - tm.Lock() - defer tm.Unlock() - - tm.transactions.Set(trans.id, trans) - tm.index.Set(tm.genIndexKeyByTrans(trans), trans) -} - -// delete removes a transaction from transactionManager. -func (tm *transactionManager) delete(transID string) { - v, ok := tm.transactions.Get(transID) - if !ok { - return - } - - tm.Lock() - defer tm.Unlock() - - trans := v.(*transaction) - tm.transactions.Delete(trans.id) - tm.index.Delete(tm.genIndexKeyByTrans(trans)) -} - -// len returns how many transactions are requesting now. -func (tm *transactionManager) len() int { - return tm.transactions.Len() -} - -// transaction returns a transaction. keyType should be one of 0, 1 which -// represents transId and index each. -func (tm *transactionManager) transaction(key string, keyType int) *transaction { - - sm := tm.transactions - if keyType == 1 { - sm = tm.index - } - - v, ok := sm.Get(key) - if !ok { - return nil - } - - return v.(*transaction) -} - -// getByTransID returns a transaction by transID. -func (tm *transactionManager) getByTransID(transID string) *transaction { - return tm.transaction(transID, 0) -} - -// getByIndex returns a transaction by indexed key. -func (tm *transactionManager) getByIndex(index string) *transaction { - return tm.transaction(index, 1) -} - -// transaction gets the proper transaction with whose id is transId and -// address is addr. -func (tm *transactionManager) filterOne(transID string, addr *net.UDPAddr) *transaction { - trans := tm.getByTransID(transID) - if trans == nil || trans.node.addr.String() != addr.String() { - return nil - } - return trans -} - -// query sends the query-formed data to udp and wait for the response. -// When timeout, it will retry `try - 1` times, which means it will query -// `try` times totally. -func (tm *transactionManager) query(q *query, try int) { - trans := tm.newTransaction(q.request.ID, q) - - tm.insert(trans) - defer tm.delete(trans.id) - - success := false - for i := 0; i < try; i++ { - if err := send(tm.dht, q.node.addr, q.request); err != nil { - break - } - - select { - case <-trans.response: - success = true - break - case <-time.After(time.Second * 15): - } - } - - if !success && q.node.id != nil { - tm.dht.routingTable.RemoveByAddr(q.node.addr.String()) - } -} - -// run starts to listen and consume the query chan. -func (tm *transactionManager) run() { - var q *query - - for { - select { - case q = <-tm.queryChan: - go tm.query(q, tm.dht.Try) - } - } -} - -// sendQuery send query-formed data to the chan. -func (tm *transactionManager) sendQuery(no *node, request Request) { - // If the target is self, then stop. - if no.id != nil && no.id.RawString() == tm.dht.node.id.RawString() || - tm.getByIndex(tm.genIndexKey(request.Method, no.addr.String())) != nil { - - return - } - - request.ID = tm.genTransID() - request.NodeID = tm.dht.node.id.RawString() - tm.queryChan <- &query{node: no, request: request} -} - -// ping sends ping query to the chan. -func (tm *transactionManager) ping(no *node) { - tm.sendQuery(no, Request{Method: pingMethod}) -} - -// findNode sends find_node query to the chan. -func (tm *transactionManager) findNode(no *node, target string) { - tm.sendQuery(no, Request{Method: findNodeMethod, Args: []string{target}}) -} - -// handle handles packets received from udp. -func handle(dht *DHT, pkt packet) { - log.Infof("Received message from %s: %s", pkt.raddr.IP.String(), string(pkt.data)) - if len(dht.workerTokens) == dht.PacketWorkerLimit { - return - } - - dht.workerTokens <- struct{}{} - - go func() { - defer func() { - <-dht.workerTokens - }() - - var data map[string]interface{} - err := bencode.DecodeBytes(pkt.data, &data) - if err != nil { - log.Errorf("Error decoding data: %s\n%s", err, pkt.data) - return - } - - msgType, ok := data[headerTypeField] - if !ok { - log.Errorf("Decoded data has no message type: %s", data) - return - } - - switch msgType.(int64) { - case requestType: - request := Request{ - ID: data[headerMessageIDField].(string), - NodeID: data[headerNodeIDField].(string), - Method: data[headerPayloadField].(string), - Args: getArgs(data[headerArgsField]), - } - spew.Dump(request) - handleRequest(dht, pkt.raddr, request) - - case responseType: - response := Response{ - ID: data[headerMessageIDField].(string), - NodeID: data[headerNodeIDField].(string), - } - - if reflect.TypeOf(data[headerPayloadField]).Kind() == reflect.String { - response.Data = data[headerPayloadField].(string) - } else { - response.FindNodeData = getFindNodeResponse(data[headerPayloadField]) - } - - spew.Dump(response) - - handleResponse(dht, pkt.raddr, response) - - case errorType: - e := Error{ - ID: data[headerMessageIDField].(string), - NodeID: data[headerNodeIDField].(string), - ExceptionType: data[headerPayloadField].(string), - Response: getArgs(data[headerArgsField]), - } - handleError(dht, pkt.raddr, e) - - default: - log.Errorf("Invalid message type: %s", msgType) - return - } - }() -} - -func getFindNodeResponse(i interface{}) (data []findNodeDatum) { - if reflect.TypeOf(i).Kind() != reflect.Slice { - return - } - - v := reflect.ValueOf(i) - for i := 0; i < v.Len(); i++ { - if v.Index(i).Kind() != reflect.Interface { - continue - } - - contact := v.Index(i).Elem() - if contact.Type().Kind() != reflect.Slice || contact.Len() != 3 { - continue - } - - if contact.Index(0).Elem().Kind() != reflect.String || - contact.Index(1).Elem().Kind() != reflect.String || - !(contact.Index(2).Elem().Kind() == reflect.Int64 || - contact.Index(2).Elem().Kind() == reflect.Int) { - continue - } - - data = append(data, findNodeDatum{ - ID: contact.Index(0).Elem().String(), - IP: contact.Index(1).Elem().String(), - Port: int(contact.Index(2).Elem().Int()), - }) - } - return -} - -func getArgs(argsInt interface{}) (args []string) { - if reflect.TypeOf(argsInt).Kind() == reflect.Slice { - v := reflect.ValueOf(argsInt) - for i := 0; i < v.Len(); i++ { - args = append(args, cast.ToString(v.Index(i).Interface())) - } - } - return -} - -// handleRequest handles the requests received from udp. -func handleRequest(dht *DHT, addr *net.UDPAddr, request Request) (success bool) { - if request.NodeID == dht.node.id.RawString() { - return - } - - if len(request.NodeID) != nodeIDLength { - send(dht, addr, Error{ID: request.ID, NodeID: dht.node.id.RawString(), Response: []string{"Invalid ID"}}) - return - } - - if no, ok := dht.routingTable.GetNodeByAddress(addr.String()); ok && no.id.RawString() != request.NodeID { - dht.routingTable.RemoveByAddr(addr.String()) - send(dht, addr, Error{ID: request.ID, NodeID: dht.node.id.RawString(), Response: []string{"Invalid ID"}}) - return - } - - switch request.Method { - case pingMethod: - send(dht, addr, Response{ID: request.ID, NodeID: dht.node.id.RawString(), Data: "pong"}) - case findNodeMethod: - if len(request.Args) < 1 { - send(dht, addr, Error{ID: request.ID, NodeID: dht.node.id.RawString(), Response: []string{"No target"}}) - return - } - - target := request.Args[0] - if len(target) != nodeIDLength { - send(dht, addr, Error{ID: request.ID, NodeID: dht.node.id.RawString(), Response: []string{"Invalid target"}}) - return - } - - nodes := []findNodeDatum{} - targetID := newBitmapFromString(target) - - no, _ := dht.routingTable.GetNodeKBucktByID(targetID) - if no != nil { - nodes = []findNodeDatum{{ID: no.id.RawString(), IP: no.addr.IP.String(), Port: no.addr.Port}} - } else { - neighbors := dht.routingTable.GetNeighbors(targetID, dht.K) - for _, n := range neighbors { - nodes = append(nodes, findNodeDatum{ID: n.id.RawString(), IP: n.addr.IP.String(), Port: n.addr.Port}) - } - } - - send(dht, addr, Response{ID: request.ID, NodeID: dht.node.id.RawString(), FindNodeData: nodes}) - - default: - // send(dht, addr, makeError(t, protocolError, "invalid q")) - return - } - - no, _ := newNode(request.NodeID, addr.Network(), addr.String()) - dht.routingTable.Insert(no) - return true -} - -// findOn puts nodes in the response to the routingTable, then if target is in -// the nodes or all nodes are in the routingTable, it stops. Otherwise it -// continues to findNode or getPeers. -func findOn(dht *DHT, nodes []findNodeDatum, target *bitmap, queryType string) error { - hasNew, found := false, false - for _, n := range nodes { - no, err := newNode(n.ID, dht.Network, fmt.Sprintf("%s:%d", n.IP, n.Port)) - if err != nil { - return err - } - - if no.id.RawString() == target.RawString() { - found = true - } - - if dht.routingTable.Insert(no) { - hasNew = true - } - } - - if found || !hasNew { - return nil - } - - targetID := target.RawString() - for _, no := range dht.routingTable.GetNeighbors(target, dht.K) { - switch queryType { - case findNodeMethod: - dht.transactionManager.findNode(no, targetID) - default: - panic("invalid find type") - } - } - return nil -} - -// handleResponse handles responses received from udp. -func handleResponse(dht *DHT, addr *net.UDPAddr, response Response) (success bool) { - trans := dht.transactionManager.filterOne(response.ID, addr) - if trans == nil { - return - } - - // If response's node id is not the same with the node id in the - // transaction, raise error. - // TODO: is this necessary??? why?? - if trans.node.id != nil && trans.node.id.RawString() != response.NodeID { - dht.routingTable.RemoveByAddr(addr.String()) - return - } - - node, err := newNode(response.NodeID, addr.Network(), addr.String()) - if err != nil { - return - } - - switch trans.request.Method { - case pingMethod: - case findNodeMethod: - target := trans.request.Args[0] - if findOn(dht, response.FindNodeData, newBitmapFromString(target), findNodeMethod) != nil { - return - } - default: - return - } - - // inform transManager to delete transaction. - trans.response <- struct{}{} - - dht.routingTable.Insert(node) - - return true -} - -// handleError handles errors received from udp. -func handleError(dht *DHT, addr *net.UDPAddr, e Error) (success bool) { - if trans := dht.transactionManager.filterOne(e.ID, addr); trans != nil { - trans.response <- struct{}{} - } - - return true -} diff --git a/dht/main_test.go b/dht/main_test.go deleted file mode 100644 index 0571e1b..0000000 --- a/dht/main_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package dht - -import ( - "math/rand" - "strconv" - "testing" - "time" -) - -func TestDHT(t *testing.T) { - rand.Seed(time.Now().UnixNano()) - - port := 49449 // + (rand.Int() % 10) - - config := NewStandardConfig() - config.Address = "127.0.0.1:" + strconv.Itoa(port) - config.PrimeNodes = []string{ - "127.0.0.1:10001", - } - - d := New(config) - t.Log("Starting...") - go d.Run() - - time.Sleep(2 * time.Second) - - for { - peers, err := d.FindNode("012b66fc7052d9a0c8cb563b8ede7662003ba65f425c2661b5c6919d445deeb31469be8b842d6faeea3f2b3ebcaec845") - if err != nil { - time.Sleep(time.Second * 1) - continue - } - - t.Log("Found peers:", peers) - break - } - - t.Error("failed") -} diff --git a/dht/routingtable.go b/dht/routingtable.go deleted file mode 100644 index eaeae16..0000000 --- a/dht/routingtable.go +++ /dev/null @@ -1,597 +0,0 @@ -package dht - -import ( - "container/heap" - "encoding/hex" - "fmt" - log "github.com/sirupsen/logrus" - "net" - "strings" - "sync" - "time" -) - -// maxPrefixLength is the length of DHT node. -const maxPrefixLength = 160 -const nodeIDLength = 48 -const compactNodeInfoLength = nodeIDLength + 6 - -// node represents a DHT node. -type node struct { - id *bitmap - addr *net.UDPAddr - lastActiveTime time.Time -} - -// newNode returns a node pointer. -func newNode(id, network, address string) (*node, error) { - if len(id) != nodeIDLength { - return nil, fmt.Errorf("node id should be a %d-length string", nodeIDLength) - } - - addr, err := net.ResolveUDPAddr(network, address) - if err != nil { - return nil, err - } - - return &node{newBitmapFromString(id), addr, time.Now()}, nil -} - -// newNodeFromCompactInfo parses compactNodeInfo and returns a node pointer. -func newNodeFromCompactInfo(compactNodeInfo string, network string) (*node, error) { - - if len(compactNodeInfo) != compactNodeInfoLength { - return nil, fmt.Errorf("compactNodeInfo should be a %d-length string", compactNodeInfoLength) - } - - id := compactNodeInfo[:nodeIDLength] - ip, port, _ := decodeCompactIPPortInfo(compactNodeInfo[nodeIDLength:]) - - return newNode(id, network, genAddress(ip.String(), port)) -} - -// CompactIPPortInfo returns "Compact IP-address/port info". -// See http://www.bittorrent.org/beps/bep_0005.html. -func (node *node) CompactIPPortInfo() string { - info, _ := encodeCompactIPPortInfo(node.addr.IP, node.addr.Port) - return info -} - -// CompactNodeInfo returns "Compact node info". -// See http://www.bittorrent.org/beps/bep_0005.html. -func (node *node) CompactNodeInfo() string { - return strings.Join([]string{ - node.id.RawString(), node.CompactIPPortInfo(), - }, "") -} - -func (node *node) HexID() string { - if node.id == nil { - return "" - } - return hex.EncodeToString([]byte(node.id.RawString())) -} - -// Peer represents a peer contact. -type Peer struct { - IP net.IP - Port int - token string -} - -// newPeer returns a new peer pointer. -func newPeer(ip net.IP, port int, token string) *Peer { - return &Peer{ - IP: ip, - Port: port, - token: token, - } -} - -// newPeerFromCompactIPPortInfo create a peer pointer by compact ip/port info. -func newPeerFromCompactIPPortInfo(compactInfo, token string) (*Peer, error) { - ip, port, err := decodeCompactIPPortInfo(compactInfo) - if err != nil { - return nil, err - } - - return newPeer(ip, port, token), nil -} - -// CompactIPPortInfo returns "Compact node info". -// See http://www.bittorrent.org/beps/bep_0005.html. -func (p *Peer) CompactIPPortInfo() string { - info, _ := encodeCompactIPPortInfo(p.IP, p.Port) - return info -} - -// peersManager represents a proxy that manipulates peers. -type peersManager struct { - sync.RWMutex - table *syncedMap - dht *DHT -} - -// newPeersManager returns a new peersManager. -func newPeersManager(dht *DHT) *peersManager { - return &peersManager{ - table: newSyncedMap(), - dht: dht, - } -} - -// Insert adds a peer into peersManager. -func (pm *peersManager) Insert(infoHash string, peer *Peer) { - pm.Lock() - if _, ok := pm.table.Get(infoHash); !ok { - pm.table.Set(infoHash, newKeyedDeque()) - } - pm.Unlock() - - v, _ := pm.table.Get(infoHash) - queue := v.(*keyedDeque) - - queue.Push(peer.CompactIPPortInfo(), peer) - if queue.Len() > pm.dht.K { - queue.Remove(queue.Front()) - } -} - -// GetPeers returns size-length peers who announces having infoHash. -func (pm *peersManager) GetPeers(infoHash string, size int) []*Peer { - peers := make([]*Peer, 0, size) - - v, ok := pm.table.Get(infoHash) - if !ok { - return peers - } - - for e := range v.(*keyedDeque).Iter() { - peers = append(peers, e.Value.(*Peer)) - } - - if len(peers) > size { - peers = peers[len(peers)-size:] - } - return peers -} - -// kbucket represents a k-size bucket. -type kbucket struct { - sync.RWMutex - nodes, candidates *keyedDeque - lastChanged time.Time - prefix *bitmap -} - -// newKBucket returns a new kbucket pointer. -func newKBucket(prefix *bitmap) *kbucket { - bucket := &kbucket{ - nodes: newKeyedDeque(), - candidates: newKeyedDeque(), - lastChanged: time.Now(), - prefix: prefix, - } - return bucket -} - -// LastChanged return the last time when it changes. -func (bucket *kbucket) LastChanged() time.Time { - bucket.RLock() - defer bucket.RUnlock() - - return bucket.lastChanged -} - -// RandomChildID returns a random id that has the same prefix with bucket. -func (bucket *kbucket) RandomChildID() string { - prefixLen := bucket.prefix.Size / 8 - - return strings.Join([]string{ - bucket.prefix.RawString()[:prefixLen], - randomString(nodeIDLength - prefixLen), - }, "") -} - -// UpdateTimestamp update bucket's last changed time.. -func (bucket *kbucket) UpdateTimestamp() { - bucket.Lock() - defer bucket.Unlock() - - bucket.lastChanged = time.Now() -} - -// Insert inserts node to the bucket. It returns whether the node is new in -// the bucket. -func (bucket *kbucket) Insert(no *node) bool { - isNew := !bucket.nodes.HasKey(no.id.RawString()) - - bucket.nodes.Push(no.id.RawString(), no) - bucket.UpdateTimestamp() - - return isNew -} - -// Replace removes node, then put bucket.candidates.Back() to the right -// place of bucket.nodes. -func (bucket *kbucket) Replace(no *node) { - bucket.nodes.Delete(no.id.RawString()) - bucket.UpdateTimestamp() - - if bucket.candidates.Len() == 0 { - return - } - - no = bucket.candidates.Remove(bucket.candidates.Back()).(*node) - - inserted := false - for e := range bucket.nodes.Iter() { - if e.Value.(*node).lastActiveTime.After( - no.lastActiveTime) && !inserted { - - bucket.nodes.InsertBefore(no, e) - inserted = true - } - } - - if !inserted { - bucket.nodes.PushBack(no) - } -} - -// Fresh pings the expired nodes in the bucket. -func (bucket *kbucket) Fresh(dht *DHT) { - for e := range bucket.nodes.Iter() { - no := e.Value.(*node) - if time.Since(no.lastActiveTime) > dht.NodeExpriedAfter { - dht.transactionManager.ping(no) - } - } -} - -// routingTableNode represents routing table tree node. -type routingTableNode struct { - sync.RWMutex - children []*routingTableNode - bucket *kbucket -} - -// newRoutingTableNode returns a new routingTableNode pointer. -func newRoutingTableNode(prefix *bitmap) *routingTableNode { - return &routingTableNode{ - children: make([]*routingTableNode, 2), - bucket: newKBucket(prefix), - } -} - -// Child returns routingTableNode's left or right child. -func (tableNode *routingTableNode) Child(index int) *routingTableNode { - if index >= 2 { - return nil - } - - tableNode.RLock() - defer tableNode.RUnlock() - - return tableNode.children[index] -} - -// SetChild sets routingTableNode's left or right child. When index is 0, it's -// the left child, if 1, it's the right child. -func (tableNode *routingTableNode) SetChild(index int, c *routingTableNode) { - tableNode.Lock() - defer tableNode.Unlock() - - tableNode.children[index] = c -} - -// KBucket returns the bucket routingTableNode holds. -func (tableNode *routingTableNode) KBucket() *kbucket { - tableNode.RLock() - defer tableNode.RUnlock() - - return tableNode.bucket -} - -// SetKBucket sets the bucket. -func (tableNode *routingTableNode) SetKBucket(bucket *kbucket) { - tableNode.Lock() - defer tableNode.Unlock() - - tableNode.bucket = bucket -} - -// Split splits current routingTableNode and sets it's two children. -func (tableNode *routingTableNode) Split() { - prefixLen := tableNode.KBucket().prefix.Size - - if prefixLen == maxPrefixLength { - return - } - - for i := 0; i < 2; i++ { - tableNode.SetChild(i, newRoutingTableNode(newBitmapFrom( - tableNode.KBucket().prefix, prefixLen+1))) - } - - tableNode.Lock() - tableNode.children[1].bucket.prefix.Set(prefixLen) - tableNode.Unlock() - - for e := range tableNode.KBucket().nodes.Iter() { - nd := e.Value.(*node) - tableNode.Child(nd.id.Bit(prefixLen)).KBucket().nodes.PushBack(nd) - } - - for e := range tableNode.KBucket().candidates.Iter() { - nd := e.Value.(*node) - tableNode.Child(nd.id.Bit(prefixLen)).KBucket().candidates.PushBack(nd) - } - - for i := 0; i < 2; i++ { - tableNode.Child(i).KBucket().UpdateTimestamp() - } -} - -// routingTable implements the routing table in DHT protocol. -type routingTable struct { - *sync.RWMutex - k int - root *routingTableNode - cachedNodes *syncedMap - cachedKBuckets *keyedDeque - dht *DHT - clearQueue *syncedList -} - -// newRoutingTable returns a new routingTable pointer. -func newRoutingTable(k int, dht *DHT) *routingTable { - root := newRoutingTableNode(newBitmap(0)) - - rt := &routingTable{ - RWMutex: &sync.RWMutex{}, - k: k, - root: root, - cachedNodes: newSyncedMap(), - cachedKBuckets: newKeyedDeque(), - dht: dht, - clearQueue: newSyncedList(), - } - - rt.cachedKBuckets.Push(root.bucket.prefix.String(), root.bucket) - return rt -} - -// Insert adds a node to routing table. It returns whether the node is new -// in the routingtable. -func (rt *routingTable) Insert(nd *node) bool { - rt.Lock() - defer rt.Unlock() - - log.Infof("Adding node to routing table: %s (%s:%d)", nd.id.RawString(), nd.addr.IP, nd.addr.Port) - - var ( - next *routingTableNode - bucket *kbucket - ) - root := rt.root - - for prefixLen := 1; prefixLen <= maxPrefixLength; prefixLen++ { - next = root.Child(nd.id.Bit(prefixLen - 1)) - - if next != nil { - // If next is not the leaf. - root = next - } else if root.KBucket().nodes.Len() < rt.k || - root.KBucket().nodes.HasKey(nd.id.RawString()) { - - bucket = root.KBucket() - isNew := bucket.Insert(nd) - - rt.cachedNodes.Set(nd.addr.String(), nd) - rt.cachedKBuckets.Push(bucket.prefix.String(), bucket) - - return isNew - } else if root.KBucket().prefix.Compare(nd.id, prefixLen-1) == 0 { - // If node has the same prefix with bucket, split it. - - root.Split() - - rt.cachedKBuckets.Delete(root.KBucket().prefix.String()) - root.SetKBucket(nil) - - for i := 0; i < 2; i++ { - bucket = root.Child(i).KBucket() - rt.cachedKBuckets.Push(bucket.prefix.String(), bucket) - } - - root = root.Child(nd.id.Bit(prefixLen - 1)) - } else { - // Finally, store node as a candidate and fresh the bucket. - root.KBucket().candidates.PushBack(nd) - if root.KBucket().candidates.Len() > rt.k { - root.KBucket().candidates.Remove( - root.KBucket().candidates.Front()) - } - - go root.KBucket().Fresh(rt.dht) - return false - } - } - return false -} - -// GetNeighbors returns the size-length nodes closest to id. -func (rt *routingTable) GetNeighbors(id *bitmap, size int) []*node { - rt.RLock() - nodes := make([]interface{}, 0, rt.cachedNodes.Len()) - for item := range rt.cachedNodes.Iter() { - nodes = append(nodes, item.val.(*node)) - } - rt.RUnlock() - - neighbors := getTopK(nodes, id, size) - result := make([]*node, len(neighbors)) - - for i, nd := range neighbors { - result[i] = nd.(*node) - } - return result -} - -// GetNeighborIds return the size-length compact node info closest to id. -func (rt *routingTable) GetNeighborCompactInfos(id *bitmap, size int) []string { - neighbors := rt.GetNeighbors(id, size) - infos := make([]string, len(neighbors)) - - for i, no := range neighbors { - infos[i] = no.CompactNodeInfo() - } - - return infos -} - -// GetNodeKBucktById returns node whose id is `id` and the bucket it -// belongs to. -func (rt *routingTable) GetNodeKBucktByID(id *bitmap) ( - nd *node, bucket *kbucket) { - - rt.RLock() - defer rt.RUnlock() - - var next *routingTableNode - root := rt.root - - for prefixLen := 1; prefixLen <= maxPrefixLength; prefixLen++ { - next = root.Child(id.Bit(prefixLen - 1)) - if next == nil { - v, ok := root.KBucket().nodes.Get(id.RawString()) - if !ok { - return - } - nd, bucket = v.Value.(*node), root.KBucket() - return - } - root = next - } - return -} - -// GetNodeByAddress finds node by address. -func (rt *routingTable) GetNodeByAddress(address string) (no *node, ok bool) { - rt.RLock() - defer rt.RUnlock() - - v, ok := rt.cachedNodes.Get(address) - if ok { - no = v.(*node) - } - return -} - -// Remove deletes the node whose id is `id`. -func (rt *routingTable) Remove(id *bitmap) { - if nd, bucket := rt.GetNodeKBucktByID(id); nd != nil { - bucket.Replace(nd) - rt.cachedNodes.Delete(nd.addr.String()) - rt.cachedKBuckets.Push(bucket.prefix.String(), bucket) - } -} - -// Remove deletes the node whose address is `ip:port`. -func (rt *routingTable) RemoveByAddr(address string) { - v, ok := rt.cachedNodes.Get(address) - if ok { - rt.Remove(v.(*node).id) - } -} - -// Fresh sends findNode to all nodes in the expired nodes. -func (rt *routingTable) Fresh() { - now := time.Now() - - for e := range rt.cachedKBuckets.Iter() { - bucket := e.Value.(*kbucket) - if now.Sub(bucket.LastChanged()) < rt.dht.KBucketExpiredAfter || - bucket.nodes.Len() == 0 { - continue - } - - i := 0 - for e := range bucket.nodes.Iter() { - if i < rt.dht.RefreshNodeNum { - no := e.Value.(*node) - rt.dht.transactionManager.findNode(no, bucket.RandomChildID()) - rt.clearQueue.PushBack(no) - } - i++ - } - } - - rt.clearQueue.Clear() -} - -// Len returns the number of nodes in table. -func (rt *routingTable) Len() int { - rt.RLock() - defer rt.RUnlock() - - return rt.cachedNodes.Len() -} - -// Implementation of heap with heap.Interface. -type heapItem struct { - distance *bitmap - value interface{} -} - -type topKHeap []*heapItem - -func (kHeap topKHeap) Len() int { - return len(kHeap) -} - -func (kHeap topKHeap) Less(i, j int) bool { - return kHeap[i].distance.Compare(kHeap[j].distance, maxPrefixLength) == 1 -} - -func (kHeap topKHeap) Swap(i, j int) { - kHeap[i], kHeap[j] = kHeap[j], kHeap[i] -} - -func (kHeap *topKHeap) Push(x interface{}) { - *kHeap = append(*kHeap, x.(*heapItem)) -} - -func (kHeap *topKHeap) Pop() interface{} { - n := len(*kHeap) - x := (*kHeap)[n-1] - *kHeap = (*kHeap)[:n-1] - return x -} - -// getTopK solves the top-k problem with heap. It's time complexity is -// O(n*log(k)). When n is large, time complexity will be too high, need to be -// optimized. -func getTopK(queue []interface{}, id *bitmap, k int) []interface{} { - topkHeap := make(topKHeap, 0, k+1) - - for _, value := range queue { - node := value.(*node) - item := &heapItem{ - id.Xor(node.id), - value, - } - heap.Push(&topkHeap, item) - if topkHeap.Len() > k { - heap.Pop(&topkHeap) - } - } - - tops := make([]interface{}, topkHeap.Len()) - for i := len(tops) - 1; i >= 0; i-- { - tops[i] = heap.Pop(&topkHeap).(*heapItem).value - } - - return tops -} diff --git a/dht/util.go b/dht/util.go deleted file mode 100644 index 0215dd6..0000000 --- a/dht/util.go +++ /dev/null @@ -1,133 +0,0 @@ -package dht - -import ( - "crypto/rand" - "errors" - "io/ioutil" - "net" - "net/http" - "strconv" - "strings" - "time" -) - -// randomString generates a size-length string randomly. -func randomString(size int) string { - buff := make([]byte, size) - rand.Read(buff) - return string(buff) -} - -// bytes2int returns the int value it represents. -func bytes2int(data []byte) uint64 { - n, val := len(data), uint64(0) - if n > 8 { - panic("data too long") - } - - for i, b := range data { - val += uint64(b) << uint64((n-i-1)*8) - } - return val -} - -// int2bytes returns the byte array it represents. -func int2bytes(val uint64) []byte { - data, j := make([]byte, 8), -1 - for i := 0; i < 8; i++ { - shift := uint64((7 - i) * 8) - data[i] = byte((val & (0xff << shift)) >> shift) - - if j == -1 && data[i] != 0 { - j = i - } - } - - if j != -1 { - return data[j:] - } - return data[:1] -} - -// decodeCompactIPPortInfo decodes compactIP-address/port info in BitTorrent -// DHT Protocol. It returns the ip and port number. -func decodeCompactIPPortInfo(info string) (ip net.IP, port int, err error) { - if len(info) != 6 { - err = errors.New("compact info should be 6-length long") - return - } - - ip = net.IPv4(info[0], info[1], info[2], info[3]) - port = int((uint16(info[4]) << 8) | uint16(info[5])) - return -} - -// encodeCompactIPPortInfo encodes an ip and a port number to -// compactIP-address/port info. -func encodeCompactIPPortInfo(ip net.IP, port int) (info string, err error) { - if port > 65535 || port < 0 { - err = errors.New("port should be no greater than 65535 and no less than 0") - return - } - - p := int2bytes(uint64(port)) - if len(p) < 2 { - p = append(p, p[0]) - p[0] = 0 - } - - info = string(append(ip, p...)) - return -} - -// getLocalIPs returns local ips. -func getLocalIPs() (ips []string) { - ips = make([]string, 0, 6) - - addrs, err := net.InterfaceAddrs() - if err != nil { - return - } - - for _, addr := range addrs { - ip, _, err := net.ParseCIDR(addr.String()) - if err != nil { - continue - } - ips = append(ips, ip.String()) - } - return -} - -// getRemoteIP returns the wlan ip. -func getRemoteIP() (ip string, err error) { - client := &http.Client{ - Timeout: time.Second * 30, - } - - req, err := http.NewRequest("GET", "http://ifconfig.me", nil) - if err != nil { - return - } - - req.Header.Set("User-Agent", "curl") - res, err := client.Do(req) - if err != nil { - return - } - - defer res.Body.Close() - - data, err := ioutil.ReadAll(res.Body) - if err != nil { - return - } - ip = string(data) - - return -} - -// genAddress returns a ip:port address. -func genAddress(ip string, port int) string { - return strings.Join([]string{ip, strconv.Itoa(port)}, ":") -} diff --git a/dht/util_test.go b/dht/util_test.go deleted file mode 100644 index 726db8f..0000000 --- a/dht/util_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package dht - -import ( - "testing" -) - -func TestInt2Bytes(t *testing.T) { - cases := []struct { - in uint64 - out []byte - }{ - {0, []byte{0}}, - {1, []byte{1}}, - {256, []byte{1, 0}}, - {22129, []byte{86, 113}}, - } - - for _, c := range cases { - r := int2bytes(c.in) - if len(r) != len(c.out) { - t.Fail() - } - - for i, v := range r { - if v != c.out[i] { - t.Fail() - } - } - } -} - -func TestBytes2Int(t *testing.T) { - cases := []struct { - in []byte - out uint64 - }{ - {[]byte{0}, 0}, - {[]byte{1}, 1}, - {[]byte{1, 0}, 256}, - {[]byte{86, 113}, 22129}, - } - - for _, c := range cases { - if bytes2int(c.in) != c.out { - t.Fail() - } - } -} - -func TestDecodeCompactIPPortInfo(t *testing.T) { - cases := []struct { - in string - out struct { - ip string - port int - } - }{ - {"123456", struct { - ip string - port int - }{"49.50.51.52", 13622}}, - {"abcdef", struct { - ip string - port int - }{"97.98.99.100", 25958}}, - } - - for _, item := range cases { - ip, port, err := decodeCompactIPPortInfo(item.in) - if err != nil || ip.String() != item.out.ip || port != item.out.port { - t.Fail() - } - } -} - -func TestEncodeCompactIPPortInfo(t *testing.T) { - cases := []struct { - in struct { - ip []byte - port int - } - out string - }{ - {struct { - ip []byte - port int - }{[]byte{49, 50, 51, 52}, 13622}, "123456"}, - {struct { - ip []byte - port int - }{[]byte{97, 98, 99, 100}, 25958}, "abcdef"}, - } - - for _, item := range cases { - info, err := encodeCompactIPPortInfo(item.in.ip, item.in.port) - if err != nil || info != item.out { - t.Fail() - } - } -} diff --git a/api/server.go b/extras/api/server.go similarity index 98% rename from api/server.go rename to extras/api/server.go index 322450f..e43aeba 100644 --- a/api/server.go +++ b/extras/api/server.go @@ -6,9 +6,9 @@ import ( "reflect" "strings" - "github.com/lbryio/lbry.go/errors" - "github.com/lbryio/lbry.go/util" - "github.com/lbryio/lbry.go/validator" + "github.com/lbryio/lbry.go/extras/errors" + "github.com/lbryio/lbry.go/extras/util" + "github.com/lbryio/lbry.go/extras/validator" v "github.com/lbryio/ozzo-validation" "github.com/spf13/cast" diff --git a/crypto/crypto.go b/extras/crypto/crypto.go similarity index 96% rename from crypto/crypto.go rename to extras/crypto/crypto.go index 10e1166..840d9a8 100644 --- a/crypto/crypto.go +++ b/extras/crypto/crypto.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/lbryio/lbry.go/errors" + "github.com/lbryio/lbry.go/extras/errors" "github.com/btcsuite/btcutil/base58" "golang.org/x/crypto/sha3" diff --git a/errors/README.md b/extras/errors/README.md similarity index 100% rename from errors/README.md rename to extras/errors/README.md diff --git a/errors/errors.go b/extras/errors/errors.go similarity index 100% rename from errors/errors.go rename to extras/errors/errors.go diff --git a/jsonrpc/daemon.go b/extras/jsonrpc/daemon.go similarity index 99% rename from jsonrpc/daemon.go rename to extras/jsonrpc/daemon.go index db7d0c1..91c746f 100644 --- a/jsonrpc/daemon.go +++ b/extras/jsonrpc/daemon.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/lbryio/lbry.go/errors" + "github.com/lbryio/lbry.go/extras/errors" "github.com/mitchellh/mapstructure" "github.com/shopspring/decimal" diff --git a/jsonrpc/daemon_test.go b/extras/jsonrpc/daemon_test.go similarity index 100% rename from jsonrpc/daemon_test.go rename to extras/jsonrpc/daemon_test.go diff --git a/jsonrpc/daemon_types.go b/extras/jsonrpc/daemon_types.go similarity index 99% rename from jsonrpc/daemon_types.go rename to extras/jsonrpc/daemon_types.go index 936fcd4..3bd04d4 100644 --- a/jsonrpc/daemon_types.go +++ b/extras/jsonrpc/daemon_types.go @@ -4,7 +4,7 @@ import ( "encoding/json" "reflect" - "github.com/lbryio/lbry.go/errors" + "github.com/lbryio/lbry.go/extras/errors" lbryschema "github.com/lbryio/types/go" "github.com/shopspring/decimal" diff --git a/null/LICENSE b/extras/null/LICENSE similarity index 100% rename from null/LICENSE rename to extras/null/LICENSE diff --git a/null/README.md b/extras/null/README.md similarity index 100% rename from null/README.md rename to extras/null/README.md diff --git a/null/bool.go b/extras/null/bool.go similarity index 100% rename from null/bool.go rename to extras/null/bool.go diff --git a/null/bool_test.go b/extras/null/bool_test.go similarity index 100% rename from null/bool_test.go rename to extras/null/bool_test.go diff --git a/null/byte.go b/extras/null/byte.go similarity index 100% rename from null/byte.go rename to extras/null/byte.go diff --git a/null/byte_test.go b/extras/null/byte_test.go similarity index 100% rename from null/byte_test.go rename to extras/null/byte_test.go diff --git a/null/bytes.go b/extras/null/bytes.go similarity index 100% rename from null/bytes.go rename to extras/null/bytes.go diff --git a/null/bytes_test.go b/extras/null/bytes_test.go similarity index 100% rename from null/bytes_test.go rename to extras/null/bytes_test.go diff --git a/null/convert/convert.go b/extras/null/convert/convert.go similarity index 100% rename from null/convert/convert.go rename to extras/null/convert/convert.go diff --git a/null/convert/convert_test.go b/extras/null/convert/convert_test.go similarity index 99% rename from null/convert/convert_test.go rename to extras/null/convert/convert_test.go index f837485..6b0ed29 100644 --- a/null/convert/convert_test.go +++ b/extras/null/convert/convert_test.go @@ -234,7 +234,7 @@ func TestConversions(t *testing.T) { if bp, boolTest := ct.d.(*bool); boolTest && *bp != ct.wantbool && ct.wanterr == "" { errf("want bool %v, got %v", ct.wantbool, *bp) } - if !ct.wanttime.IsNull() && !ct.wanttime.Equal(getTimeValue(ct.d)) { + if !ct.wanttime.IsZero() && !ct.wanttime.Equal(getTimeValue(ct.d)) { errf("want time %v, got %v", ct.wanttime, getTimeValue(ct.d)) } if ct.wantnil && *ct.d.(**int64) != nil { diff --git a/null/float32.go b/extras/null/float32.go similarity index 100% rename from null/float32.go rename to extras/null/float32.go diff --git a/null/float32_test.go b/extras/null/float32_test.go similarity index 100% rename from null/float32_test.go rename to extras/null/float32_test.go diff --git a/null/float64.go b/extras/null/float64.go similarity index 100% rename from null/float64.go rename to extras/null/float64.go diff --git a/null/float64_test.go b/extras/null/float64_test.go similarity index 100% rename from null/float64_test.go rename to extras/null/float64_test.go diff --git a/null/int.go b/extras/null/int.go similarity index 100% rename from null/int.go rename to extras/null/int.go diff --git a/null/int16.go b/extras/null/int16.go similarity index 100% rename from null/int16.go rename to extras/null/int16.go diff --git a/null/int16_test.go b/extras/null/int16_test.go similarity index 100% rename from null/int16_test.go rename to extras/null/int16_test.go diff --git a/null/int32.go b/extras/null/int32.go similarity index 100% rename from null/int32.go rename to extras/null/int32.go diff --git a/null/int32_test.go b/extras/null/int32_test.go similarity index 100% rename from null/int32_test.go rename to extras/null/int32_test.go diff --git a/null/int64.go b/extras/null/int64.go similarity index 100% rename from null/int64.go rename to extras/null/int64.go diff --git a/null/int64_test.go b/extras/null/int64_test.go similarity index 100% rename from null/int64_test.go rename to extras/null/int64_test.go diff --git a/null/int8.go b/extras/null/int8.go similarity index 100% rename from null/int8.go rename to extras/null/int8.go diff --git a/null/int8_test.go b/extras/null/int8_test.go similarity index 100% rename from null/int8_test.go rename to extras/null/int8_test.go diff --git a/null/int_test.go b/extras/null/int_test.go similarity index 100% rename from null/int_test.go rename to extras/null/int_test.go diff --git a/null/json.go b/extras/null/json.go similarity index 100% rename from null/json.go rename to extras/null/json.go diff --git a/null/json_test.go b/extras/null/json_test.go similarity index 100% rename from null/json_test.go rename to extras/null/json_test.go diff --git a/null/nullable.go b/extras/null/nullable.go similarity index 100% rename from null/nullable.go rename to extras/null/nullable.go diff --git a/null/string.go b/extras/null/string.go similarity index 100% rename from null/string.go rename to extras/null/string.go diff --git a/null/string_test.go b/extras/null/string_test.go similarity index 100% rename from null/string_test.go rename to extras/null/string_test.go diff --git a/null/time.go b/extras/null/time.go similarity index 100% rename from null/time.go rename to extras/null/time.go diff --git a/null/time_test.go b/extras/null/time_test.go similarity index 100% rename from null/time_test.go rename to extras/null/time_test.go diff --git a/null/uint.go b/extras/null/uint.go similarity index 100% rename from null/uint.go rename to extras/null/uint.go diff --git a/null/uint16.go b/extras/null/uint16.go similarity index 100% rename from null/uint16.go rename to extras/null/uint16.go diff --git a/null/uint16_test.go b/extras/null/uint16_test.go similarity index 100% rename from null/uint16_test.go rename to extras/null/uint16_test.go diff --git a/null/uint32.go b/extras/null/uint32.go similarity index 100% rename from null/uint32.go rename to extras/null/uint32.go diff --git a/null/uint32_test.go b/extras/null/uint32_test.go similarity index 100% rename from null/uint32_test.go rename to extras/null/uint32_test.go diff --git a/null/uint64.go b/extras/null/uint64.go similarity index 100% rename from null/uint64.go rename to extras/null/uint64.go diff --git a/null/uint64_test.go b/extras/null/uint64_test.go similarity index 100% rename from null/uint64_test.go rename to extras/null/uint64_test.go diff --git a/null/uint8.go b/extras/null/uint8.go similarity index 100% rename from null/uint8.go rename to extras/null/uint8.go diff --git a/null/uint8_test.go b/extras/null/uint8_test.go similarity index 100% rename from null/uint8_test.go rename to extras/null/uint8_test.go diff --git a/null/uint_test.go b/extras/null/uint_test.go similarity index 100% rename from null/uint_test.go rename to extras/null/uint_test.go diff --git a/query/query.go b/extras/query/query.go similarity index 98% rename from query/query.go rename to extras/query/query.go index 7edbdbb..e085f32 100644 --- a/query/query.go +++ b/extras/query/query.go @@ -8,8 +8,8 @@ import ( "strings" "time" - "github.com/lbryio/lbry.go/errors" - "github.com/lbryio/lbry.go/null" + "github.com/lbryio/lbry.go/extras/errors" + "github.com/lbryio/lbry.go/extras/null" ) func InterpolateParams(query string, args ...interface{}) (string, error) { diff --git a/stop/readme.md b/extras/stop/readme.md similarity index 100% rename from stop/readme.md rename to extras/stop/readme.md diff --git a/stop/stop.go b/extras/stop/stop.go similarity index 100% rename from stop/stop.go rename to extras/stop/stop.go diff --git a/travis/travis.go b/extras/travis/travis.go similarity index 98% rename from travis/travis.go rename to extras/travis/travis.go index c04db2e..abe6a57 100644 --- a/travis/travis.go +++ b/extras/travis/travis.go @@ -23,7 +23,7 @@ import ( "encoding/pem" "net/http" - "github.com/lbryio/lbry.go/errors" + "github.com/lbryio/lbry.go/extras/errors" ) func publicKey(isPrivateRepo bool) (*rsa.PublicKey, error) { diff --git a/travis/webhook.go b/extras/travis/webhook.go similarity index 100% rename from travis/webhook.go rename to extras/travis/webhook.go diff --git a/util/slack.go b/extras/util/slack.go similarity index 97% rename from util/slack.go rename to extras/util/slack.go index c735886..cb75d74 100644 --- a/util/slack.go +++ b/extras/util/slack.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/lbryio/lbry.go/errors" + "github.com/lbryio/lbry.go/extras/errors" "github.com/nlopes/slack" log "github.com/sirupsen/logrus" diff --git a/util/slice.go b/extras/util/slice.go similarity index 100% rename from util/slice.go rename to extras/util/slice.go diff --git a/util/underscore.go b/extras/util/underscore.go similarity index 100% rename from util/underscore.go rename to extras/util/underscore.go diff --git a/validator/bool.go b/extras/validator/bool.go similarity index 100% rename from validator/bool.go rename to extras/validator/bool.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0b54842 --- /dev/null +++ b/go.mod @@ -0,0 +1,50 @@ +module github.com/lbryio/lbry.go + +require ( + github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf // indirect + github.com/btcsuite/btcd v0.0.0-20180531025944-86fed781132a + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect + github.com/btcsuite/btcutil v0.0.0-20180524032703-d4cc87b86016 + github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect + github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect + github.com/davecgh/go-spew v1.1.0 + github.com/go-errors/errors v1.0.1 + github.com/go-ini/ini v1.38.2 + github.com/go-ozzo/ozzo-validation v3.5.0+incompatible // indirect + github.com/golang/protobuf v1.2.0 + github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect + github.com/gorilla/context v1.1.1 // indirect + github.com/gorilla/mux v1.6.2 + github.com/gorilla/rpc v1.1.0 + github.com/gorilla/websocket v1.2.0 // indirect + github.com/jtolds/gls v4.2.1+incompatible // indirect + github.com/lbryio/errors.go v0.0.0-20180223142025-ad03d3cc6a5c + github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04 + github.com/lbryio/types v0.0.0-20181001180206-594241d24e00 + github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect + github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 // indirect + github.com/lyoshenka/bencode v0.0.0-20180323155644-b7abd7672df5 + github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675 + github.com/nlopes/slack v0.2.0 + github.com/onsi/gomega v1.4.3 // indirect + github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561 + github.com/sergi/go-diff v1.0.0 + github.com/shopspring/decimal v0.0.0-20180607144847-19e3cb6c2930 + github.com/sirupsen/logrus v0.0.0-20180523074243-ea8897e79973 + github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect + github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect + github.com/spf13/cast v1.2.0 + github.com/stretchr/testify v1.3.0 // indirect + github.com/uber-go/atomic v1.3.2 + github.com/ybbus/jsonrpc v0.0.0-20180411222309-2a548b7d822d + go.uber.org/atomic v1.3.2 // indirect + golang.org/x/crypto v0.0.0-20180608092829-8ac0e0d97ce4 + golang.org/x/net v0.0.0-20180906233101-161cd47e91fd + golang.org/x/time v0.0.0-20181108054448-85acf8d2951c + google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f // indirect + google.golang.org/grpc v1.17.0 + gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect + gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect + gopkg.in/ini.v1 v1.41.0 // indirect + gopkg.in/nullbio/null.v6 v6.0.0-20161116030900-40264a2e6b79 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..614ff63 --- /dev/null +++ b/go.sum @@ -0,0 +1,131 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/btcsuite/btcd v0.0.0-20180531025944-86fed781132a h1:fuEu3WaOzatbWFGlCa2e/TL/GR397Da8QSKNKrDRa3s= +github.com/btcsuite/btcd v0.0.0-20180531025944-86fed781132a/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20180524032703-d4cc87b86016 h1:BsZAJgCuMsoFZMZNyj7Lyt6sS8anDhedVrAMCOyPMIo= +github.com/btcsuite/btcutil v0.0.0-20180524032703-d4cc87b86016/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-ini/ini v1.38.2 h1:6Hl/z3p3iFkA0dlDfzYxuFuUGD+kaweypF6btsR2/Q4= +github.com/go-ini/ini v1.38.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-ozzo/ozzo-validation v3.5.0+incompatible h1:sUy/in/P6askYr16XJgTKq/0SZhiWsdg4WZGaLsGQkM= +github.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/rpc v1.1.0 h1:marKfvVP0Gpd/jHlVBKCQ8RAoUPdX7K1Nuh6l1BNh7A= +github.com/gorilla/rpc v1.1.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= +github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= +github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/lbryio/errors.go v0.0.0-20180223142025-ad03d3cc6a5c h1:BhdcWGsuKif/XoSZnqVGNqJ1iEmH0czWR5upj+AuR8M= +github.com/lbryio/errors.go v0.0.0-20180223142025-ad03d3cc6a5c/go.mod h1:muH7wpUqE8hRA3OrYYosw9+Sl681BF9cwcjzE+OCNK8= +github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04 h1:Nze+C2HbeKvhjI/kVn+9Poj/UuEW5sOQxcsxqO7L3GI= +github.com/lbryio/ozzo-validation v0.0.0-20170323141101-d1008ad1fd04/go.mod h1:fbG/dzobG8r95KzMwckXiLMHfFjZaBRQqC9hPs2XAQ4= +github.com/lbryio/types v0.0.0-20181001180206-594241d24e00 h1:1qRpd8lcyVigX+kYkwQL13gpOURyytgvxZtuIQfPPX8= +github.com/lbryio/types v0.0.0-20181001180206-594241d24e00/go.mod h1:CG3wsDv5BiVYQd5i1Jp7wGsaVyjZTJshqXeWMVKsISE= +github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 h1:AsEBgzv3DhuYHI/GiQh2HxvTP71HCCE9E/tzGUzGdtU= +github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5/go.mod h1:c2mYKRyMb1BPkO5St0c/ps62L4S0W2NAkaTXj9qEI+0= +github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 h1:iOAVXzZyXtW408TMYejlUPo6BIn92HmOacWtIfNyYns= +github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg= +github.com/lyoshenka/bencode v0.0.0-20180323155644-b7abd7672df5 h1:mG83tLXWSRdcXMWfkoumVwhcCbf3jHF9QKv/m37BkM0= +github.com/lyoshenka/bencode v0.0.0-20180323155644-b7abd7672df5/go.mod h1:H0aPCWffGOaDcjkw1iB7W9DVLp6GXmfcJY/7YZCWPA4= +github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675 h1:/rdJjIiKG5rRdwG5yxHmSE/7ZREjpyC0kL7GxGT/qJw= +github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/nlopes/slack v0.2.0 h1:ygNVH3HWrOPFbzFoAmRKPcMcmYMmsLf+vPV9DhJdqJI= +github.com/nlopes/slack v0.2.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= +github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561 h1:IY+sDBJR/wRtsxq+626xJnt4Tw7/ROA9cDIR8MMhWyg= +github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561/go.mod h1:lvjGftC8oe7XPtyrOidaMi0rp5B9+XY/ZRUynGnuaxQ= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shopspring/decimal v0.0.0-20180607144847-19e3cb6c2930 h1:pSgp2x9zCkCjb8rxXFNpGE8hDIrt+UXW7jUQ5fbTlzM= +github.com/shopspring/decimal v0.0.0-20180607144847-19e3cb6c2930/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/sirupsen/logrus v0.0.0-20180523074243-ea8897e79973 h1:3AJZYTzw3gm3TNTt30x0CCKD7GOn2sdd50Hn35fQkGY= +github.com/sirupsen/logrus v0.0.0-20180523074243-ea8897e79973/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg= +github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo= +github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/ybbus/jsonrpc v0.0.0-20180411222309-2a548b7d822d h1:tQo6hjclyv3RHUgZOl6iWb2Y44A/sN9bf9LAYfuioEg= +github.com/ybbus/jsonrpc v0.0.0-20180411222309-2a548b7d822d/go.mod h1:XJrh1eMSzdIYFbM08flv0wp5G35eRniyeGut1z+LSiE= +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +golang.org/x/crypto v0.0.0-20180608092829-8ac0e0d97ce4 h1:wviDUSmtheHRBfoY8B9U8ELl2USoXi2YFwdGdpIIkzI= +golang.org/x/crypto v0.0.0-20180608092829-8ac0e0d97ce4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522 h1:Ve1ORMCxvRmSXBwJK+t3Oy+V2vRW2OetUQBq4rJIkZE= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f h1:FU37niK8AQ59mHcskRyQL7H0ErSeNh650vdcj8HqdSI= +google.golang.org/genproto v0.0.0-20181004005441-af9cb2a35e7f/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/ini.v1 v1.41.0 h1:Ka3ViY6gNYSKiVy71zXBEqKplnV35ImDLVG+8uoIklE= +gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/nullbio/null.v6 v6.0.0-20161116030900-40264a2e6b79 h1:FpCr9V8wuOei4BAen+93HtVJ+XSi+KPbaPKm0Vj5R64= +gopkg.in/nullbio/null.v6 v6.0.0-20161116030900-40264a2e6b79/go.mod h1:gWkaRU7CoXpezCBWfWjm3999QqS+1pYPXGbqQCTMzo8= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/img/filewatchers.png b/img/filewatchers.png deleted file mode 100644 index acd0ed9b6e14ad63170450d450da167b80fc40d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20983 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