first commit
This commit is contained in:
commit
6eefab86da
|
|
@ -0,0 +1,12 @@
|
||||||
|
/intent-system.exe
|
||||||
|
/intent-system
|
||||||
|
/doc/~$集群管理系统角色和权限明细.xlsx
|
||||||
|
/.idea
|
||||||
|
/static/static/
|
||||||
|
/static/favicon.ico
|
||||||
|
/static/index.html
|
||||||
|
/node_modules
|
||||||
|
/cmd/intent-system
|
||||||
|
/cmd/intent-system.exe
|
||||||
|
/go.work
|
||||||
|
/go.work.local
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
FROM golang:1.21 AS builder
|
||||||
|
MAINTAINER lory <civet148@outlook.com>
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y ca-certificates make
|
||||||
|
ENV SRC_DIR /intent-system
|
||||||
|
RUN set -x \
|
||||||
|
&& cd /tmp
|
||||||
|
|
||||||
|
RUN go env -w GOPROXY=https://goproxy.io
|
||||||
|
|
||||||
|
COPY . $SRC_DIR
|
||||||
|
RUN cd $SRC_DIR && export GIT_SSL_NO_VERIFY=true && git config --global http.sslVerify "false" && make
|
||||||
|
|
||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
#RUN ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo "Asia/Shanghai" > /etc/timezone && apt-get update && apt-get install -y tzdata
|
||||||
|
#ENV TZ Asia/Shanghai
|
||||||
|
ENV SRC_DIR /intent-system
|
||||||
|
|
||||||
|
|
||||||
|
# 管理系统主程序
|
||||||
|
COPY --from=builder $SRC_DIR/intent-system /usr/local/bin/intent-system
|
||||||
|
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||||
|
|
||||||
|
|
||||||
|
ENV HOME_PATH /data
|
||||||
|
|
||||||
|
VOLUME $HOME_PATH
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
#SHELL=/usr/bin/env bash
|
||||||
|
|
||||||
|
CLEAN:=
|
||||||
|
BINS:=
|
||||||
|
DATE_TIME=`date +'%Y%m%d %H:%M:%S'`
|
||||||
|
COMMIT_ID=`git rev-parse --short HEAD`
|
||||||
|
MANAGER_DIR=${PWD}
|
||||||
|
CONSOLE_CODE=/tmp/intent-system-frontend
|
||||||
|
|
||||||
|
build:
|
||||||
|
rm -f intent-system
|
||||||
|
go mod tidy && go build -ldflags "-s -w -X 'main.BuildTime=${DATE_TIME}' -X 'main.GitCommit=${COMMIT_ID}'" -o intent-system cmd/main.go
|
||||||
|
.PHONY: build
|
||||||
|
BINS+=intent-system
|
||||||
|
|
||||||
|
nodejs:
|
||||||
|
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - && sudo apt update && sudo apt install -y nodejs build-essential && sudo npm install -g yarn
|
||||||
|
.PHONY: nodejs
|
||||||
|
|
||||||
|
console:
|
||||||
|
rm -rf ${CONSOLE_CODE} && git clone -b master https://git.your-enterprise.com/intent-system-frontend.git ${CONSOLE_CODE}
|
||||||
|
cd ${CONSOLE_CODE} && git log -2 && npm install && npm run build:prod
|
||||||
|
.PHONY: console
|
||||||
|
|
||||||
|
docker-test: build
|
||||||
|
docker build --tag intent-system -f Dockerfile.test .
|
||||||
|
.PHONY: docker-test
|
||||||
|
|
||||||
|
docker:
|
||||||
|
rm -f intent-system
|
||||||
|
docker build --tag intent-system -f Dockerfile .
|
||||||
|
.PHONY: docker
|
||||||
|
|
||||||
|
# 检查环境变量
|
||||||
|
env-%:
|
||||||
|
@ if [ "${${*}}" = "" ]; then \
|
||||||
|
echo "Environment variable $* not set"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
db2go:
|
||||||
|
go install github.com/civet148/db2go@latest
|
||||||
|
.PHONY: db2go
|
||||||
|
|
||||||
|
models:
|
||||||
|
cd pkg/dal/db2go && ./gen_models.sh
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(CLEAN) $(BINS)
|
||||||
|
.PHONY: clean
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"intent-system/pkg/api"
|
||||||
|
"intent-system/pkg/config"
|
||||||
|
"intent-system/pkg/itypes"
|
||||||
|
"intent-system/pkg/services"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
|
||||||
|
"github.com/civet148/log"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Version = "v0.5.11"
|
||||||
|
ProgramName = "intent-system"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
BuildTime = "2024-05-11"
|
||||||
|
GitCommit = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CMD_NAME_RUN = "run"
|
||||||
|
CMD_NAME_START = "start"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CMD_FLAG_NAME_DSN = "dsn"
|
||||||
|
CMD_FLAG_NAME_POSTGRESQL = "pg"
|
||||||
|
CMD_FLAG_NAME_DEBUG = "debug"
|
||||||
|
CMD_FLAG_NAME_STATIC = "static"
|
||||||
|
CMD_FLAG_NAME_DOMAIN = "domain"
|
||||||
|
CMD_FLAG_NAME_IMAGE_PATH = "image-path"
|
||||||
|
CMD_FLAG_NAME_IMAGE_PREFIX = "image-prefix"
|
||||||
|
CMD_FLAG_NAME_GATEWAY_URL = "gateway-url"
|
||||||
|
CMD_FLAG_NAME_GATEWAY_KEY = "gateway-key"
|
||||||
|
CMD_FLAG_NAME_GATEWAY_SECRET = "gateway-secret"
|
||||||
|
CMD_FLAG_NAME_SUB_CRON = "sub-cron"
|
||||||
|
)
|
||||||
|
|
||||||
|
var manager api.ManagerApi
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func grace() {
|
||||||
|
//capture signal of Ctrl+C and gracefully exit
|
||||||
|
sigChannel := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChannel, os.Interrupt)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case s := <-sigChannel:
|
||||||
|
{
|
||||||
|
if s != nil && s == os.Interrupt {
|
||||||
|
fmt.Printf("Ctrl+C signal captured, program exiting...\n")
|
||||||
|
if manager != nil {
|
||||||
|
manager.Close()
|
||||||
|
}
|
||||||
|
close(sigChannel)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
grace()
|
||||||
|
|
||||||
|
local := []*cli.Command{
|
||||||
|
runCmd,
|
||||||
|
}
|
||||||
|
app := &cli.App{
|
||||||
|
Name: ProgramName,
|
||||||
|
Version: fmt.Sprintf("%s %s commit %s", Version, BuildTime, GitCommit),
|
||||||
|
Flags: []cli.Flag{},
|
||||||
|
Commands: local,
|
||||||
|
Action: nil,
|
||||||
|
}
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Errorf("exit in error %s", err)
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var runCmd = &cli.Command{
|
||||||
|
Name: CMD_NAME_RUN,
|
||||||
|
Usage: "run as a web service",
|
||||||
|
ArgsUsage: "[listen address]",
|
||||||
|
Aliases: []string{CMD_NAME_START},
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: CMD_FLAG_NAME_DEBUG,
|
||||||
|
Usage: "open debug log mode",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: CMD_FLAG_NAME_DSN,
|
||||||
|
Usage: "data source name of database",
|
||||||
|
Value: itypes.DEFAULT_DATA_SOURCE_NAME,
|
||||||
|
Aliases: []string{"n"},
|
||||||
|
},
|
||||||
|
//&cli.StringFlag{
|
||||||
|
// Name: CMD_FLAG_NAME_STATIC,
|
||||||
|
// Usage: "frontend static path",
|
||||||
|
// Value: itypes.DefaultStaticHome,
|
||||||
|
//},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: CMD_FLAG_NAME_IMAGE_PATH,
|
||||||
|
Usage: "image saving path",
|
||||||
|
Value: itypes.DefaultImagesHome,
|
||||||
|
Aliases: []string{"i"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: CMD_FLAG_NAME_DOMAIN,
|
||||||
|
Usage: "domain url",
|
||||||
|
Required: true,
|
||||||
|
Aliases: []string{"d"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: CMD_FLAG_NAME_IMAGE_PREFIX,
|
||||||
|
Usage: "image url prefix",
|
||||||
|
Value: itypes.DEFAULT_IMAGE_PREFIX,
|
||||||
|
Aliases: []string{"p"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: CMD_FLAG_NAME_GATEWAY_URL,
|
||||||
|
Usage: "sdk gateway url",
|
||||||
|
Aliases: []string{"g"},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: CMD_FLAG_NAME_GATEWAY_KEY,
|
||||||
|
Usage: "sdk gateway access key",
|
||||||
|
Aliases: []string{"k"},
|
||||||
|
Required: true,
|
||||||
|
}, &cli.StringFlag{
|
||||||
|
Name: CMD_FLAG_NAME_GATEWAY_SECRET,
|
||||||
|
Usage: "sdk gateway access secret",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: CMD_FLAG_NAME_POSTGRESQL,
|
||||||
|
Usage: "Postgresql connection string for news sync",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: CMD_FLAG_NAME_SUB_CRON,
|
||||||
|
Usage: "cron task for email subscription",
|
||||||
|
Value: itypes.DEFAULT_SUB_CRON_EMAIL_PUSH,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(cctx *cli.Context) error {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Version: Version,
|
||||||
|
HttpAddr: itypes.DEFAULT_HTTP_LISTEN_ADDR,
|
||||||
|
DSN: cctx.String(CMD_FLAG_NAME_DSN),
|
||||||
|
Debug: cctx.Bool(CMD_FLAG_NAME_DEBUG),
|
||||||
|
Domain: cctx.String(CMD_FLAG_NAME_DOMAIN),
|
||||||
|
Static: cctx.String(CMD_FLAG_NAME_STATIC),
|
||||||
|
ImagePath: cctx.String(CMD_FLAG_NAME_IMAGE_PATH),
|
||||||
|
ImagePrefix: cctx.String(CMD_FLAG_NAME_IMAGE_PREFIX),
|
||||||
|
GatewayUrl: cctx.String(CMD_FLAG_NAME_GATEWAY_URL),
|
||||||
|
GatewayKey: cctx.String(CMD_FLAG_NAME_GATEWAY_KEY),
|
||||||
|
GatewaySecret: cctx.String(CMD_FLAG_NAME_GATEWAY_SECRET),
|
||||||
|
Postgresql: cctx.String(CMD_FLAG_NAME_POSTGRESQL),
|
||||||
|
SubCron: cctx.String(CMD_FLAG_NAME_SUB_CRON),
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Version = Version
|
||||||
|
if cfg.Debug {
|
||||||
|
log.SetLevel("debug")
|
||||||
|
}
|
||||||
|
log.Json("configuration", cfg)
|
||||||
|
if cctx.Args().First() != "" {
|
||||||
|
cfg.HttpAddr = cctx.Args().First()
|
||||||
|
}
|
||||||
|
if err := cfg.Save(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
//start up as a web server
|
||||||
|
manager = services.NewManager(cfg)
|
||||||
|
return manager.Run()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
USE `intent-system`;
|
||||||
|
|
||||||
|
insert into `tag` (`name`, `name_cn`, `is_inherent`, `is_deleted`) values('#AI','#人工智能','1','0');
|
||||||
|
insert into `tag` (`name`, `name_cn`, `is_inherent`, `is_deleted`) values('#Blockchain','#区块链','1','0');
|
||||||
|
|
||||||
|
INSERT INTO `dictionary` (`name`, `config_key`, `config_value`, `remark`, `deleted`) VALUES ('[SMTP] server', 'smtp_server', 'mail.jellydropsllc.com', '', '0');
|
||||||
|
INSERT INTO `dictionary` (`name`, `config_key`, `config_value`, `remark`, `deleted`) VALUES ('[SMTP] port', 'smtp_port', '465', '', '0');
|
||||||
|
INSERT INTO `dictionary` (`name`, `config_key`, `config_value`, `remark`, `deleted`) VALUES ('[SMTP] email address', 'smtp_name', 'it@jellydropsllc.com', '', '0');
|
||||||
|
INSERT INTO `dictionary` (`name`, `config_key`, `config_value`, `remark`, `deleted`) VALUES ('[SMTP] auth code', 'auth_code', 'Z[yj4ri1tWRM', '', '0');
|
||||||
|
INSERT INTO `dictionary` (`name`, `config_key`, `config_value`, `remark`, `deleted`) VALUES ('[SMTP] send name', 'send_name', 'Jelly AI', '', '0');
|
||||||
|
|
@ -0,0 +1,409 @@
|
||||||
|
/*
|
||||||
|
SQLyog Trial v13.1.8 (64 bit)
|
||||||
|
MySQL - 8.0.23 : Database - intent-system
|
||||||
|
*********************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!40101 SET NAMES utf8 */;
|
||||||
|
|
||||||
|
/*!40101 SET SQL_MODE=''*/;
|
||||||
|
|
||||||
|
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||||
|
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||||
|
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||||
|
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||||
|
CREATE DATABASE /*!32312 IF NOT EXISTS*/`intent-system` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
|
||||||
|
|
||||||
|
USE `intent-system`;
|
||||||
|
|
||||||
|
/*Table structure for table `casbin_rule` */
|
||||||
|
|
||||||
|
CREATE TABLE `casbin_rule` (
|
||||||
|
`p_type` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
|
||||||
|
`v0` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
|
||||||
|
`v1` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
|
||||||
|
`v2` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
|
||||||
|
`v3` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
|
||||||
|
`v4` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
|
||||||
|
`v5` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
|
||||||
|
KEY `IDX_casbin_rule_v0` (`v0`) USING BTREE,
|
||||||
|
KEY `IDX_casbin_rule_v1` (`v1`) USING BTREE,
|
||||||
|
KEY `IDX_casbin_rule_v2` (`v2`) USING BTREE,
|
||||||
|
KEY `IDX_casbin_rule_v3` (`v3`) USING BTREE,
|
||||||
|
KEY `IDX_casbin_rule_v4` (`v4`) USING BTREE,
|
||||||
|
KEY `IDX_casbin_rule_v5` (`v5`) USING BTREE,
|
||||||
|
KEY `IDX_casbin_rule_p_type` (`p_type`) USING BTREE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
|
/*Table structure for table `customer` */
|
||||||
|
|
||||||
|
CREATE TABLE `customer` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID(自增)',
|
||||||
|
`user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '登录名称',
|
||||||
|
`user_alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '账户别名',
|
||||||
|
`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '登录密码(MD5+SALT)',
|
||||||
|
`first_name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '姓',
|
||||||
|
`last_name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名',
|
||||||
|
`title` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '职称',
|
||||||
|
`company` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '公司名称',
|
||||||
|
`salt` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'MD5加密盐',
|
||||||
|
`phone_number` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '联系手机号',
|
||||||
|
`is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为超级管理员(0=普通账户 1=超级管理员)',
|
||||||
|
`email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '邮箱地址',
|
||||||
|
`address` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '家庭住址/公司地址',
|
||||||
|
`remark` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
|
||||||
|
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)',
|
||||||
|
`state` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已冻结(1=已启用 2=已冻结)',
|
||||||
|
`is_subscribed` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已订阅(0=未订阅 1=已订阅)',
|
||||||
|
`login_ip` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最近登录IP',
|
||||||
|
`login_time` bigint NOT NULL DEFAULT '0' COMMENT '最近登录时间',
|
||||||
|
`create_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人',
|
||||||
|
`edit_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最近编辑人',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE KEY `UNIQ_USER_NAME` (`user_name`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='客户信息表';
|
||||||
|
|
||||||
|
/*Table structure for table `dictionary` */
|
||||||
|
|
||||||
|
CREATE TABLE `dictionary` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名称',
|
||||||
|
`config_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'KEY',
|
||||||
|
`config_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'VALUE',
|
||||||
|
`remark` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
|
||||||
|
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE KEY `key` (`config_key`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
|
/*Table structure for table `invite_code` */
|
||||||
|
|
||||||
|
CREATE TABLE `invite_code` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`user_id` int NOT NULL COMMENT '注册用户ID',
|
||||||
|
`user_acc` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '注册账户',
|
||||||
|
`random_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '认证码(5位字母和数字组合)',
|
||||||
|
`link_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '链接URL(保留字段)',
|
||||||
|
`state` tinyint(1) NOT NULL COMMENT '状态(1=等待校验 2=已校验)',
|
||||||
|
`expire_time` bigint NOT NULL DEFAULT '0' COMMENT '过期时间(UNIX时间戳)',
|
||||||
|
`action_type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '操作类型(0=注册 1=重置密码)',
|
||||||
|
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `UNIQ_INVITECODE` (`user_acc`,`random_code`,`deleted`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
/*Table structure for table `login` */
|
||||||
|
|
||||||
|
CREATE TABLE `login` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`login_type` tinyint NOT NULL DEFAULT '0' COMMENT '登录类型(0=管理用户 1=注册用户)',
|
||||||
|
`user_id` int NOT NULL COMMENT '登录用户ID',
|
||||||
|
`login_ip` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '登录IP',
|
||||||
|
`login_addr` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '登录地址',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='登录记录表';
|
||||||
|
|
||||||
|
/*Table structure for table `news` */
|
||||||
|
|
||||||
|
CREATE TABLE `news` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`org_id` bigint NOT NULL DEFAULT '0' COMMENT 'AI文章同步ID',
|
||||||
|
`spider_id` bigint NOT NULL DEFAULT '0' COMMENT '爬虫文章ID',
|
||||||
|
`tag` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '文章标签(原始标签)',
|
||||||
|
`category` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '分类',
|
||||||
|
`main_title` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '主标题',
|
||||||
|
`sub_title` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '副标题',
|
||||||
|
`summary` text COLLATE utf8mb4_unicode_ci COMMENT '摘要',
|
||||||
|
`keywords` text COLLATE utf8mb4_unicode_ci COMMENT '文章关键词',
|
||||||
|
`seo_keywords` text COLLATE utf8mb4_unicode_ci COMMENT 'SEO关键词',
|
||||||
|
`tags` json DEFAULT NULL COMMENT '人工打标签(多选)',
|
||||||
|
`url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文章链接',
|
||||||
|
`image_url` text COLLATE utf8mb4_unicode_ci COMMENT '图片URL',
|
||||||
|
`content` longtext COLLATE utf8mb4_unicode_ci COMMENT '文章内容',
|
||||||
|
`is_hotspot` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否热门(0=否 1=是)',
|
||||||
|
`is_overwritten` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已被覆盖(0=否 1=是)',
|
||||||
|
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=否 1=是)',
|
||||||
|
`is_replicate` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为副本(0=否 1=是)',
|
||||||
|
`state` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态(0=未发布订阅 1=已发布订阅 2=已推送)',
|
||||||
|
`language` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '语言(zh-CN=中文 en=英文)',
|
||||||
|
`data_time` timestamp NOT NULL COMMENT '数据生成时间',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '数据创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '数据更新时间',
|
||||||
|
`extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `INDEX_MAIN_TITLE` (`main_title`),
|
||||||
|
KEY `INDEX_SUB_TITLE` (`sub_title`),
|
||||||
|
KEY `INDEX_CREATED_TIME` (`created_time` DESC),
|
||||||
|
KEY `INDEX_TAG` (`tag`),
|
||||||
|
KEY `INDEX_HOTSPOT` (`is_hotspot`,`is_overwritten`,`is_deleted`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='新闻文章数据表(AI编辑)';
|
||||||
|
|
||||||
|
/*Table structure for table `news_draft` */
|
||||||
|
|
||||||
|
CREATE TABLE `news_draft` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`news_id` bigint NOT NULL DEFAULT '0' COMMENT '新闻ID(对应news表id字段)',
|
||||||
|
`org_id` bigint NOT NULL DEFAULT '0' COMMENT '源新闻ID(对应news表org_id字段)',
|
||||||
|
`category` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '分类',
|
||||||
|
`main_title` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '主标题',
|
||||||
|
`sub_title` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '副标题',
|
||||||
|
`summary` text COLLATE utf8mb4_unicode_ci COMMENT '摘要',
|
||||||
|
`keywords` text COLLATE utf8mb4_unicode_ci COMMENT '关键字(JSON数组)',
|
||||||
|
`seo_keywords` text COLLATE utf8mb4_unicode_ci COMMENT 'SEO关键字(JSON数组)',
|
||||||
|
`tags` json DEFAULT NULL COMMENT '标签(JSON数组)',
|
||||||
|
`image_url` text COLLATE utf8mb4_unicode_ci COMMENT '图片URL',
|
||||||
|
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '文章内容',
|
||||||
|
`language` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '语言(zh-CN=中文 en=英文)',
|
||||||
|
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=否 1=是)',
|
||||||
|
`is_replicate` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为副本(0=否 1=是)',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '数据创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '数据更新时间',
|
||||||
|
`extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `INDEX_CREATED_TIME` (`created_time` DESC),
|
||||||
|
KEY `INDEX_HOTSPOT` (`is_deleted`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='草稿箱';
|
||||||
|
|
||||||
|
/*Table structure for table `news_spider` */
|
||||||
|
|
||||||
|
CREATE TABLE `news_spider` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`org_id` bigint NOT NULL DEFAULT '0' COMMENT '新闻同步ID',
|
||||||
|
`tag` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '文章标签',
|
||||||
|
`category` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '分类',
|
||||||
|
`main_title` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '主标题',
|
||||||
|
`sub_title` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '副标题',
|
||||||
|
`summary` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '摘要',
|
||||||
|
`keywords` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '文章关键词',
|
||||||
|
`seo_keywords` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT 'SEO关键词',
|
||||||
|
`url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文章链接',
|
||||||
|
`image_url` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '图片URL',
|
||||||
|
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '文章内容',
|
||||||
|
`is_hotspot` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否热门(0=否 1=是)',
|
||||||
|
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=否 1=是)',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '数据创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '数据更新时间',
|
||||||
|
`extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `INDEX_MAIN_TITLE` (`main_title`),
|
||||||
|
KEY `INDEX_SUB_TITLE` (`sub_title`),
|
||||||
|
KEY `INDEX_CREATED_TIME` (`created_time` DESC),
|
||||||
|
KEY `INDEX_TAG` (`tag`),
|
||||||
|
KEY `INDEX_UPDATED_TIME` (`updated_time` DESC),
|
||||||
|
KEY `INDEX_NEWS_ID` (`org_id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='新闻文章数据表(爬虫)';
|
||||||
|
|
||||||
|
/*Table structure for table `news_subscribe` */
|
||||||
|
|
||||||
|
CREATE TABLE `news_subscribe` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`news_id` bigint NOT NULL COMMENT '订阅推送新闻ID(对应news表id字段)',
|
||||||
|
`news_subject` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '邮件主题',
|
||||||
|
`news_url` varchar(1024) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '订阅新闻推送URL',
|
||||||
|
`is_pushed` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已推送(0=否 1=是)',
|
||||||
|
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=否 1=是)',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `INDEX_NEWS_ID` (`news_id`,`is_deleted`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
/*Table structure for table `oper_log` */
|
||||||
|
|
||||||
|
CREATE TABLE `oper_log` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`oper_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '操作用户',
|
||||||
|
`oper_type` tinyint(1) NOT NULL COMMENT '操作类型(1=首页 2=系统管理 3=存储管理 4=资源管理 5=告警中心)',
|
||||||
|
`oper_time` timestamp NOT NULL COMMENT '操作时间',
|
||||||
|
`oper_content` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '操作内容',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
|
/*Table structure for table `privilege` */
|
||||||
|
|
||||||
|
CREATE TABLE `privilege` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`category` tinyint(1) NOT NULL DEFAULT '0' COMMENT '权限分类(保留字段)',
|
||||||
|
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '权限名称',
|
||||||
|
`label` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '权限标签',
|
||||||
|
`path` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '权限访问路径',
|
||||||
|
`children` mediumtext COLLATE utf8mb4_unicode_ci COMMENT '子权限树',
|
||||||
|
`is_inherent` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否固有权限(0=否 1=是)',
|
||||||
|
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '权限备注',
|
||||||
|
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='角色-菜单关系表';
|
||||||
|
|
||||||
|
/*Table structure for table `question_answer` */
|
||||||
|
|
||||||
|
CREATE TABLE `question_answer` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`org_id` bigint NOT NULL DEFAULT '0' COMMENT 'Q&A源ID(同步ID)',
|
||||||
|
`question` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '问题',
|
||||||
|
`answer` mediumtext COLLATE utf8mb4_unicode_ci COMMENT '答案',
|
||||||
|
`state` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已发布(0=草稿 1=已发布 2=已下架)',
|
||||||
|
`language` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '语言(zh-CN=中文 en=英文)',
|
||||||
|
`is_overwritten` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已被覆盖(0=否 1=是)',
|
||||||
|
`is_replicate` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为副本(0=否 1=是)',
|
||||||
|
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)',
|
||||||
|
`data_time` timestamp NOT NULL COMMENT '数据生成时间',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `INDEX_CREATED_TIME` (`created_time` DESC),
|
||||||
|
KEY `INDEX_UPDATED_TIME` (`updated_time` DESC)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
/*Table structure for table `question_draft` */
|
||||||
|
|
||||||
|
CREATE TABLE `question_draft` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`qa_id` bigint NOT NULL DEFAULT '0' COMMENT '源ID(对应question_answer表id字段)',
|
||||||
|
`org_id` bigint NOT NULL DEFAULT '0' COMMENT 'Q&A同步ID',
|
||||||
|
`question` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '问题',
|
||||||
|
`answer` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '答案',
|
||||||
|
`language` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '语言(zh-CN=中文 en=英文)',
|
||||||
|
`is_overwritten` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已被覆盖(0=否 1=是)',
|
||||||
|
`is_replicate` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为副本(0=否 1=是)',
|
||||||
|
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `INDEX_CREATED_TIME` (`created_time` DESC),
|
||||||
|
KEY `INDEX_UPDATED_TIME` (`updated_time` DESC)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
/*Table structure for table `role` */
|
||||||
|
|
||||||
|
CREATE TABLE `role` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '角色ID(自增)',
|
||||||
|
`role_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名称',
|
||||||
|
`role_alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '角色别名',
|
||||||
|
`create_user` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人',
|
||||||
|
`edit_user` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最近编辑人',
|
||||||
|
`remark` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
|
||||||
|
`is_inherent` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否固有角色(0=自定义角色 1=平台固有角色)',
|
||||||
|
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE KEY `UNIQ_ROLE_NAME` (`role_name`) COMMENT '角色名称唯一约束'
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='角色信息表';
|
||||||
|
|
||||||
|
/*Table structure for table `run_config` */
|
||||||
|
|
||||||
|
CREATE TABLE `run_config` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT 'incr id',
|
||||||
|
`config_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'config name',
|
||||||
|
`config_key` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'config key',
|
||||||
|
`config_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT 'config value',
|
||||||
|
`remark` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'remark',
|
||||||
|
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'is deleted(0=false 1=true)',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'created time',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE KEY `UNIQ_NAME_KEY` (`config_name`,`config_key`) USING BTREE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='run config table';
|
||||||
|
|
||||||
|
/*Table structure for table `subscriber` */
|
||||||
|
|
||||||
|
CREATE TABLE `subscriber` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`customer_id` int NOT NULL DEFAULT '0' COMMENT '订阅者ID(对应customer标id字段,可为空)',
|
||||||
|
`email` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '订阅者邮箱',
|
||||||
|
`tags` varchar(1024) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '订阅标签(主题)',
|
||||||
|
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已取消订阅(0=否 1=是)',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `UNIQ_CUSTOMER_EMAIL` (`email`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
/*Table structure for table `tag` */
|
||||||
|
|
||||||
|
CREATE TABLE `tag` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`name` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标签名',
|
||||||
|
`name_cn` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '中文名',
|
||||||
|
`is_inherent` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为固有标签(0=否 1=是)',
|
||||||
|
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
/*Table structure for table `template` */
|
||||||
|
|
||||||
|
CREATE TABLE `template` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`template_type` int NOT NULL COMMENT '模板类型(1=订阅欢迎邮件[英文] 2=订阅欢迎邮件[中午])',
|
||||||
|
`subject` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主题',
|
||||||
|
`content` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '内容',
|
||||||
|
`language` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '语言(英语=en 中文=zh-CN)',
|
||||||
|
`editor_user` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最后编辑人',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
/*Table structure for table `user` */
|
||||||
|
|
||||||
|
CREATE TABLE `user` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID(自增)',
|
||||||
|
`user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '登录名称',
|
||||||
|
`user_alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '账户别名',
|
||||||
|
`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '登录密码(MD5+SALT)',
|
||||||
|
`salt` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'MD5加密盐',
|
||||||
|
`phone_number` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '联系手机号',
|
||||||
|
`is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为超级管理员(0=普通账户 1=超级管理员)',
|
||||||
|
`email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '邮箱地址',
|
||||||
|
`address` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '家庭住址/公司地址',
|
||||||
|
`remark` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
|
||||||
|
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)',
|
||||||
|
`state` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已冻结(1=已启用 2=已冻结)',
|
||||||
|
`login_ip` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最近登录IP',
|
||||||
|
`login_time` bigint NOT NULL DEFAULT '0' COMMENT '最近登录时间',
|
||||||
|
`create_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人',
|
||||||
|
`edit_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最近编辑人',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE KEY `UNIQ_USER_NAME` (`user_name`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='登录账户信息表';
|
||||||
|
|
||||||
|
/*Table structure for table `user_role` */
|
||||||
|
|
||||||
|
CREATE TABLE `user_role` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID',
|
||||||
|
`user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名',
|
||||||
|
`role_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名',
|
||||||
|
`create_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人',
|
||||||
|
`edit_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最近编辑人',
|
||||||
|
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)',
|
||||||
|
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE KEY `UNIQ_USER_NAME` (`user_name`) COMMENT '用户唯一约束'
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='用户角色关系表';
|
||||||
|
|
||||||
|
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||||
|
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||||
|
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||||
|
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#镜像地址和版本(本地)
|
||||||
|
IMAGE_URL=intent-system:latest
|
||||||
|
|
||||||
|
#容器名称
|
||||||
|
CONTAINER_NAME=intent-system
|
||||||
|
|
||||||
|
# 数据源(正式环境需修改成实际数据库配置)
|
||||||
|
DSN='mysql://root:123456@127.0.0.1:3306/intent-system?charset=utf8mb4'
|
||||||
|
PG='postgres://dong:Pg2#123321@14.17.80.241:5432/webdb?sslmode=disable&search_path=public'
|
||||||
|
|
||||||
|
# 管理系统HTTP服务监听地址
|
||||||
|
LISTEN_ADDR="0.0.0.0:8083"
|
||||||
|
|
||||||
|
# 数据挂载目录
|
||||||
|
DATA_DIR=/data/intent-system
|
||||||
|
|
||||||
|
# 订阅邮件访问链接域名
|
||||||
|
DOMAIN="http://103.39.218.177:3008/blog"
|
||||||
|
|
||||||
|
# 图片存储域名+后缀
|
||||||
|
#IMAGE_PREFIX=https://www.your-enterprise.com/images
|
||||||
|
|
||||||
|
# 网关URL
|
||||||
|
GATEWAY_URL="ws://127.0.0.1:12345"
|
||||||
|
|
||||||
|
# 网关访问KEY
|
||||||
|
GATEWAY_KEY="bAkYh0JVe2Kph0ot"
|
||||||
|
|
||||||
|
# 网关访问密码
|
||||||
|
GATEWAY_SECRET="1EWKBne2LCX0TJBXkrOWSzSDkzaQmoR3xuXBrc41JsdjorpM"
|
||||||
|
|
||||||
|
# 订阅邮件定时任务
|
||||||
|
SUB_CRON="0 0 * * *"
|
||||||
|
|
||||||
|
#删除原来的容器
|
||||||
|
docker rm -f "${CONTAINER_NAME}"
|
||||||
|
|
||||||
|
docker run -p 8083:8083 -v ${DATA_DIR}:~/.intent-system --restart always --name "${CONTAINER_NAME}" -d "$IMAGE_URL" \
|
||||||
|
intent-system run --debug -n "${DSN}" --pg "${PG}" -d "${DOMAIN}" -g "${GATEWAY_URL}" -k "${GATEWAY_KEY}" -s "${GATEWAY_SECRET}" --sub-cron "${SUB_CRON}" "$LISTEN_ADDR"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/bash
|
||||||
|
docker rm -f intent-system
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
# 部署步骤
|
||||||
|
|
||||||
|
## 1. 安装MySQL8
|
||||||
|
|
||||||
|
### 1.1 方案一 物理机安装MySQL8
|
||||||
|
|
||||||
|
- *操作系统*
|
||||||
|
|
||||||
|
*Ubuntu20.04LTS*
|
||||||
|
|
||||||
|
如果操作系统不是Ubuntu20则需要更新MySQL官方源信息
|
||||||
|
|
||||||
|
#### 1.1.1 apt安装MySQL
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ sudo apt update && sudo apt install mysql-server
|
||||||
|
|
||||||
|
```
|
||||||
|
#### 1.1.2. 检查安装后是否启动成功
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ ps -ef | grep mysqld
|
||||||
|
mysql 65492 1 0 17:46 ? 00:00:00 /usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.1.3. 修改MySQL监听地址和端口
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ sudo netstat -alnt | grep 3306
|
||||||
|
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN
|
||||||
|
|
||||||
|
# 编辑mysqld.cnf文件将bind-address对应的值由127.0.0.1改成0.0.0.0或局域网IP地址(端口视情况决定是否修改)
|
||||||
|
$ sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf
|
||||||
|
|
||||||
|
# 重启MySQL服务
|
||||||
|
$ sudo service mysql restart
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.1.4. 创建oss用户和权限
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# MySQL安装完会在/etc/mysql目录下有一个debian.cnf文件可用于本地登录并修改root密码或创建用户
|
||||||
|
$ sudo cat /etc/mysql/debian.cnf
|
||||||
|
|
||||||
|
[client]
|
||||||
|
host = localhost
|
||||||
|
user = debian-sys-maint
|
||||||
|
password = xgf1OdcBzRy0LaEP
|
||||||
|
socket = /var/run/mysqld/mysqld.sock
|
||||||
|
|
||||||
|
# 本地登录MySQL,执行下面的命令行
|
||||||
|
$ mysql -udebian-sys-maint -pxgf1OdcBzRy0LaEP mysql
|
||||||
|
|
||||||
|
mysql> select host,user,plugin,authentication_string from user;
|
||||||
|
+-----------+------------------+-----------------------+-------------------------------------------+
|
||||||
|
| host | user | plugin | authentication_string |
|
||||||
|
+-----------+------------------+-----------------------+-------------------------------------------+
|
||||||
|
| localhost | root | auth_socket | |
|
||||||
|
| localhost | mysql.session | mysql_native_password | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE |
|
||||||
|
| localhost | mysql.sys | mysql_native_password | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE |
|
||||||
|
| localhost | debian-sys-maint | mysql_native_password | *22CC5F671040F19FF9FB1E5A9B94D2576C4A1A24 |
|
||||||
|
| % | node | mysql_native_password | *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 |
|
||||||
|
+-----------+------------------+-----------------------+-------------------------------------------+
|
||||||
|
|
||||||
|
4 rows in set (0.00 sec)
|
||||||
|
|
||||||
|
# 创建一个admin账户并允许远程登录, 密码是123456(生产环境请设置复杂密码)
|
||||||
|
mysql> create user 'admin'@'%' identified by '123456';
|
||||||
|
Query OK, 0 rows affected (0.00 sec)
|
||||||
|
|
||||||
|
# 赋予oss所有权限
|
||||||
|
mysql> grant all on *.* to 'admin'@'%';
|
||||||
|
mysql> flush privileges;
|
||||||
|
|
||||||
|
# 修改root密码和口令加密方式并开启远程登录(视实际情况而定,如果无必要可以只修改密码不开启远程登录)
|
||||||
|
# host='%'表示开启远程访问,如果不开启就不要这个SQL字句
|
||||||
|
mysql> update user set plugin='mysql_native_password', authentication_string='', host='%' where user='root';
|
||||||
|
Query OK, 1 row affected (0.00 sec)
|
||||||
|
Rows matched: 1 Changed: 1 Warnings: 0
|
||||||
|
|
||||||
|
# 重置root账户登录密码并刷新权限
|
||||||
|
mysql> alter user 'root'@'%' IDENTIFIED BY '123456'; #适用于8.x版本修改密码(设置不成功可能是需要复杂密码)
|
||||||
|
mysql> flush privileges;
|
||||||
|
|
||||||
|
# 查看账户信息
|
||||||
|
mysql> select host,user,plugin,authentication_string from user;
|
||||||
|
+-----------+------------------+-----------------------+-------------------------------------------+
|
||||||
|
| host | user | plugin | authentication_string |
|
||||||
|
+-----------+------------------+-----------------------+-------------------------------------------+
|
||||||
|
| % | root | mysql_native_password | *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 |
|
||||||
|
| localhost | mysql.session | mysql_native_password | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE |
|
||||||
|
| localhost | mysql.sys | mysql_native_password | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE |
|
||||||
|
| localhost | debian-sys-maint | mysql_native_password | *22CC5F671040F19FF9FB1E5A9B94D2576C4A1A24 |
|
||||||
|
| % | admin | mysql_native_password | *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 |
|
||||||
|
+-----------+------------------+-----------------------+-------------------------------------------+
|
||||||
|
5 rows in set (0.00 sec)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 方案二 docker安装MySQL8
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 创建本地数据库目录
|
||||||
|
$ sudo mkdir -p /data/mysql/{mysql-files,conf,logs,data}
|
||||||
|
|
||||||
|
# 启动容器(设置root初始密码为123456)
|
||||||
|
$ docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 --restart always \
|
||||||
|
-e TZ=Asia/Shanghai \
|
||||||
|
-v /data/mysql/mysql-files:/var/lib/mysql-files \
|
||||||
|
-v /data/mysql/conf:/etc/mysql \
|
||||||
|
-v /data/mysql/logs:/var/log/mysql \
|
||||||
|
-v /data/mysql/data:/var/lib/mysql \
|
||||||
|
--name mysql -d mysql:8.0.23
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 登录mysql终端(手动输入初始密码123456登录MySQL控制台)
|
||||||
|
$ docker exec -it mysql mysql -uroot -p mysql
|
||||||
|
|
||||||
|
Enter password:
|
||||||
|
Reading table information for completion of table and column names
|
||||||
|
You can turn off this feature to get a quicker startup with -A
|
||||||
|
|
||||||
|
Welcome to the MySQL monitor. Commands end with ; or \g.
|
||||||
|
Your MySQL connection id is 8
|
||||||
|
Server version: 8.0.23 MySQL Community Server - GPL
|
||||||
|
|
||||||
|
Copyright (c) 2000, 2021, Oracle and/or its affiliates.
|
||||||
|
|
||||||
|
Oracle is a registered trademark of Oracle Corporation and/or its
|
||||||
|
affiliates. Other names may be trademarks of their respective
|
||||||
|
owners.
|
||||||
|
|
||||||
|
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
|
||||||
|
|
||||||
|
# 更改数据库root账户密码为123456并开启远程访问(密码可以自行修改成其他也可以保持原密码,主要是通过%符号开启root远程访问)
|
||||||
|
mysql> USE mysql;
|
||||||
|
mysql> ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
|
||||||
|
mysql> FLUSH PRIVILEGES;
|
||||||
|
mysql> exit
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 初始化数据表
|
||||||
|
|
||||||
|
将oss-manager.sql和init-data.sql文件上传到服务器/tmp目录
|
||||||
|
|
||||||
|
```shell script
|
||||||
|
# 服务器登录MySQL命令行终端执行sql文件
|
||||||
|
mysql> source /path/to/intent-system.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 解除MySQL分组查询限制
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 打开mysqld.cnf文件并在[mysqld]选项范围内加一行下面的参数(如果sql_mode已存在则去掉ONLY_FULL_GROUP_BY)
|
||||||
|
$ sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf
|
||||||
|
sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'
|
||||||
|
|
||||||
|
# 重启mysql服务
|
||||||
|
$ sudo service mysql restart
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 更改服务器/容器时区
|
||||||
|
|
||||||
|
MySQL运行服务器或容器时区改为UTC+8 (北京时间)
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
version: 2
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
machine: true
|
||||||
|
# This would be for if we didn't have machine: true. Could help with circleci local execute, which doesn't support VMs?
|
||||||
|
# docker:
|
||||||
|
# - image: cimg/go:1.13
|
||||||
|
environment:
|
||||||
|
GO_BRANCH: release-branch.go1.16
|
||||||
|
steps:
|
||||||
|
- run: echo $CIRCLE_WORKING_DIRECTORY
|
||||||
|
- run: echo $PWD
|
||||||
|
- run: echo $GOPATH
|
||||||
|
- run: echo 'export GOPATH=$HOME/go' >> $BASH_ENV
|
||||||
|
- run: echo 'export PATH="$GOPATH/bin:$PATH"' >> $BASH_ENV
|
||||||
|
- run: echo $GOPATH
|
||||||
|
- run: which go || sudo apt install golang-go
|
||||||
|
- run: go version
|
||||||
|
- run: |
|
||||||
|
cd /usr/local
|
||||||
|
sudo mkdir go.local
|
||||||
|
sudo chown `whoami` go.local
|
||||||
|
- restore_cache:
|
||||||
|
key: go-local-
|
||||||
|
- run: |
|
||||||
|
cd /usr/local
|
||||||
|
git clone git://github.com/golang/go go.local || true
|
||||||
|
cd go.local
|
||||||
|
git fetch
|
||||||
|
git checkout "$GO_BRANCH"
|
||||||
|
[[ -x bin/go && `git rev-parse HEAD` == `cat anacrolix.built` ]] && exit
|
||||||
|
cd src
|
||||||
|
./make.bash || exit
|
||||||
|
git rev-parse HEAD > ../anacrolix.built
|
||||||
|
- save_cache:
|
||||||
|
paths: /usr/local/go.local
|
||||||
|
key: go-local-{{ checksum "/usr/local/go.local/anacrolix.built" }}
|
||||||
|
- run: echo 'export PATH="/usr/local/go.local/bin:$PATH"' >> $BASH_ENV
|
||||||
|
- run: go version
|
||||||
|
- checkout
|
||||||
|
- run: sudo apt-get update
|
||||||
|
- run: sudo apt install fuse pv
|
||||||
|
- restore_cache:
|
||||||
|
keys:
|
||||||
|
- go-pkg-
|
||||||
|
- restore_cache:
|
||||||
|
keys:
|
||||||
|
- go-cache-
|
||||||
|
- run: go get -d ./...
|
||||||
|
- run: go test -v -race ./... -count 2
|
||||||
|
- run: go test -bench . ./...
|
||||||
|
- run: set +e; CGO_ENABLED=0 go test -v ./...; true
|
||||||
|
- run: GOARCH=386 go test ./... -count 2 -bench . || true
|
||||||
|
- run: go install github.com/anacrolix/godo@latest
|
||||||
|
- save_cache:
|
||||||
|
key: go-pkg-{{ checksum "go.mod" }}
|
||||||
|
paths:
|
||||||
|
- ~/go/pkg
|
||||||
|
- run: sudo modprobe fuse
|
||||||
|
- run: fs/test.sh
|
||||||
|
- save_cache:
|
||||||
|
key: go-cache-{{ .Revision }}
|
||||||
|
paths:
|
||||||
|
- ~/.cache/go-build
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
test_patterns = ["**/*_test.go"]
|
||||||
|
|
||||||
|
[[analyzers]]
|
||||||
|
name = "go"
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[analyzers.meta]
|
||||||
|
import_root = "github.com/anacrolix/torrent"
|
||||||
|
|
||||||
|
[[transformers]]
|
||||||
|
name = "gofmt"
|
||||||
|
enabled = true
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
name: 'Common Go'
|
||||||
|
description: 'Checks out, and handles Go setup and caching'
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Set up Go
|
||||||
|
if: matrix.go-version != 'tip'
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
# The OS defines the directories to use, then this is specific to go. The go version could
|
||||||
|
# affect the dependencies. The job can affect what is actually downloaded, and provides
|
||||||
|
# collision resistance. Finally, the hash of the go.sum files ensures a new cache is created
|
||||||
|
# when the dependencies change. Note if this were just a mod cache, we might do this based
|
||||||
|
# on time or something.
|
||||||
|
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ github.job }}-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-${{ matrix.go-version }}-${{ github.job }}-
|
||||||
|
${{ runner.os }}-go-${{ matrix.go-version }}-
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
- run: |
|
||||||
|
echo GOTIP_REVISION="`git ls-remote https://github.com/golang/go refs/heads/master | cut -f1`" >> "$GITHUB_ENV"
|
||||||
|
echo GOTIP_PATH="$HOME/gotip" >> "$GITHUB_ENV"
|
||||||
|
if: matrix.go-version == 'tip'
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
if: matrix.go-version == 'tip'
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ env.GOTIP_PATH }}
|
||||||
|
# The build varies by OS (and arch, but I haven't bothered to add that yet.) We always want
|
||||||
|
# the latest snapshot that works for us, the revision is only used to store differentiate
|
||||||
|
# builds.
|
||||||
|
key: gotip-ls-remote-${{ runner.os }}-${{ env.GOTIP_REVISION }}
|
||||||
|
restore-keys: |
|
||||||
|
gotip-ls-remote-${{ runner.os }}-${{ env.GOTIP_REVISION }}
|
||||||
|
gotip-ls-remote-${{ runner.os }}-
|
||||||
|
gotip-env-home-${{ runner.os }}-
|
||||||
|
gotip-${{ runner.os }}-
|
||||||
|
- name: Install gotip
|
||||||
|
if: matrix.go-version == 'tip'
|
||||||
|
run: |
|
||||||
|
git clone --depth=1 https://github.com/golang/go "$GOTIP_PATH" || true
|
||||||
|
cd "$GOTIP_PATH"
|
||||||
|
git pull
|
||||||
|
echo "GOROOT=$GOTIP_PATH" >> "$GITHUB_ENV"
|
||||||
|
echo "$(go env GOPATH)/bin:$PATH" >> "$GITHUB_PATH"
|
||||||
|
echo "$GOTIP_PATH/bin:$PATH" >> "$GITHUB_PATH"
|
||||||
|
echo "anacrolix.built:" $(cat anacrolix.built)
|
||||||
|
[[ -x bin/go && `git rev-parse HEAD` == `cat anacrolix.built` ]] && exit
|
||||||
|
cd src
|
||||||
|
./make.bash || exit
|
||||||
|
git rev-parse HEAD > ../anacrolix.built
|
||||||
|
env
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master, ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [master]
|
||||||
|
schedule:
|
||||||
|
- cron: '0 10 * * 4'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyse:
|
||||||
|
name: Analyse
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
# We must fetch at least the immediate parents so that if this is
|
||||||
|
# a pull request then we can checkout the head.
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
# If this run was triggered by a pull request event, then checkout
|
||||||
|
# the head of the pull request instead of the merge commit.
|
||||||
|
- run: git checkout HEAD^2
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
# Override language selection by uncommenting this and choosing your languages
|
||||||
|
# with:
|
||||||
|
# languages: go, javascript, csharp, python, cpp, java
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
name: Go
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
test:
|
||||||
|
timeout-minutes: 10
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [ '1.21' ]
|
||||||
|
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: ./.github/actions/go-common
|
||||||
|
- run: go test -race -count 2 $(go list ./... | grep -v /fs)
|
||||||
|
- run: go test -race -count 2 ./fs/...
|
||||||
|
if: ${{ ! contains(matrix.os, 'windows') }}
|
||||||
|
|
||||||
|
test-benchmarks:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [ '1.21' ]
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: ./.github/actions/go-common
|
||||||
|
- run: go test -race -run @ -bench . -benchtime 2x ./...
|
||||||
|
|
||||||
|
bench:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [ '1.21' ]
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: ./.github/actions/go-common
|
||||||
|
- run: go test -run @ -bench . ./...
|
||||||
|
|
||||||
|
test-386:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [ '1.21' ]
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: ./.github/actions/go-common
|
||||||
|
- run: GOARCH=386 go test ./...
|
||||||
|
- run: GOARCH=386 go test ./... -run @ -bench .
|
||||||
|
|
||||||
|
build-wasm:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [ '1.21' ]
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: ./.github/actions/go-common
|
||||||
|
- name: Some packages compile for WebAssembly
|
||||||
|
run: GOOS=js GOARCH=wasm go build . ./storage ./tracker/...
|
||||||
|
|
||||||
|
torrentfs-linux:
|
||||||
|
timeout-minutes: 5
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [ '1.21' ]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: ./.github/actions/go-common
|
||||||
|
|
||||||
|
- name: Install godo
|
||||||
|
run: |
|
||||||
|
# Need master for cross-compiling fix
|
||||||
|
go install -v -x github.com/anacrolix/godo@master
|
||||||
|
echo $PATH
|
||||||
|
|
||||||
|
- name: Apt packages
|
||||||
|
run: sudo apt install pv fuse
|
||||||
|
|
||||||
|
- name: torrentfs end-to-end test
|
||||||
|
# Test on 386 for atomic alignment and other bad 64-bit assumptions
|
||||||
|
run: GOARCH=386 fs/test.sh
|
||||||
|
|
||||||
|
# Github broke FUSE on MacOS, I'm not sure what the state is.
|
||||||
|
|
||||||
|
# torrentfs-macos:
|
||||||
|
# timeout-minutes: 15
|
||||||
|
# runs-on: ${{ matrix.os }}
|
||||||
|
# strategy:
|
||||||
|
# matrix:
|
||||||
|
# go-version: [ '1.20' ]
|
||||||
|
# os: [macos-latest]
|
||||||
|
# fail-fast: false
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/checkout@v2
|
||||||
|
# - uses: ./.github/actions/go-common
|
||||||
|
#
|
||||||
|
# - run: brew install macfuse pv md5sha1sum bash
|
||||||
|
#
|
||||||
|
# - name: Install godo
|
||||||
|
# run: go install -v github.com/anacrolix/godo@master
|
||||||
|
#
|
||||||
|
# - name: torrentfs end-to-end test
|
||||||
|
# run: fs/test.sh
|
||||||
|
# # Pretty sure macos on GitHub CI has issues with the fuse driver now.
|
||||||
|
# continue-on-error: true
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
name: GolangCI-Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ '!master' ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ '!master' ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
golint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GO111MODULE: on
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: golangci/golangci-lint-action@v2
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
# Optional: working directory, useful for monorepos
|
||||||
|
# working-directory: somedir
|
||||||
|
|
||||||
|
# Optional: golangci-lint command line arguments.
|
||||||
|
args: -D errcheck,unused,structcheck,deadcode
|
||||||
|
|
||||||
|
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||||
|
only-new-issues: true
|
||||||
|
|
||||||
|
# Optional: if set to true then the action will use pre-installed Go.
|
||||||
|
skip-go-installation: true
|
||||||
|
|
||||||
|
# Optional: if set to true then the action don't cache or restore ~/go/pkg.
|
||||||
|
# skip-pkg-cache: true
|
||||||
|
|
||||||
|
# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
|
||||||
|
# skip-build-cache: true
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
.idea
|
||||||
|
*-run.gob
|
||||||
|
.envrc*
|
||||||
|
.DS_Store
|
||||||
|
go.work*
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
linters-settings:
|
||||||
|
staticcheck:
|
||||||
|
go: "1.16"
|
||||||
|
checks: ["all", "-U1000"]
|
||||||
|
|
||||||
|
govet:
|
||||||
|
disable:
|
||||||
|
- composites
|
||||||
|
|
@ -0,0 +1,373 @@
|
||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
### Literature
|
||||||
|
|
||||||
|
* [arvid on writing a fast piece picker](https://blog.libtorrent.org/2011/11/writing-a-fast-piece-picker/)
|
||||||
|
|
||||||
|
Uses C++ for examples.
|
||||||
|
|
||||||
|
* [On Piece Selection for Streaming BitTorrent](https://www.diva-portal.org/smash/get/diva2:835742/FULLTEXT01.pdf)
|
||||||
|
|
||||||
|
Some simulations by some Swedes on piece selection.
|
||||||
|
|
||||||
|
* [A South American paper on peer-selection strategies for uploading](https://arxiv.org/pdf/1402.2187.pdf)
|
||||||
|
|
||||||
|
Has some useful overviews of piece-selection.
|
||||||
|
|
||||||
|
### Hole-punching
|
||||||
|
|
||||||
|
Holepunching is tracked in Torrent, rather than in Client because if we send a rendezvous message, and subsequently receive a connect message, we do not know if a peer sent a rendezvous message to our relay and we're receiving the connect message for their rendezvous or ours. Relays are not required to respond to rendezvous, so we can't enforce a timeout. If we don't know if who sent the rendezvous that triggered a connect, then we don't know what infohash to use in the handshake. Once we send a rendezvous, and never receive a reply, we would have to always perform handshakes with our original infohash, or always copy the infohash the remote sends. Handling connects by always being the passive side in the handshake won't work since the other side might use the same behaviour and neither will initiate.
|
||||||
|
|
||||||
|
If we only perform rendezvous through relays for the same torrent as the relay, then all the handshake can be done actively for all connect messages. All connect messages received from a peer can only be for the same torrent for which we are connected to the peer.
|
||||||
|
|
||||||
|
In 2006, approximately 70% of clients were behind NAT (https://web.archive.org/web/20100724011252/http://illuminati.coralcdn.org/stats/). According to https://fosdem.org/2023/schedule/event/network_hole_punching_in_the_wild/, hole punching (in libp2p) 70% of NAT can be defeated by relay mechanisms.
|
||||||
|
|
||||||
|
If either or both peers in a potential peer do not have NAT, or are full cone NAT, then NAT doesn't matter at least for BitTorrent, as both parties are trying to connect to each other and connections will always work in one direction.
|
||||||
|
|
||||||
|
The chance that 2 peers can connect to each other would be 1-(badnat)^2, and 1-unrelayable*(badnat)^2 where unrelayable is the chance they can't work even with a relay, and badnat is the chance a peer has a bad NAT (not full cone). For example if unrelayable is 0.3 per the libp2p study, and badnat is 0.5 (i made this up), 92.5% of peers can connect with each other if they use "relay mechanisms", and 75% if they don't. as long as any peers in the swarm are not badnat, they can relay those that are, and and act as super nodes for peers that can't or don't implement hole punching.
|
||||||
|
|
||||||
|
The DHT is a bit different: you can't be an active node if you are a badnat, but you can still query the network to get what you need, you just don't contribute to it. It also doesn't matter what the swarm looks like for a given torrent on the DHT, because you don't have to be in the swarm to host its data. all that matters is that there are some peers that aren't badnat that are in the DHT, of which there are millions (for BitTorrent).
|
||||||
|
|
||||||
|
- https://blog.ipfs.tech/2022-01-20-libp2p-hole-punching/
|
||||||
|
- https://www.bittorrent.org/beps/bep_0055.html
|
||||||
|
- https://github.com/anacrolix/torrent/issues/685
|
||||||
|
- https://stackoverflow.com/questions/38786438/libutp-%C2%B5tp-and-nat-traversal-udp-hole-punching
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
# torrent
|
||||||
|
|
||||||
|
[](https://pkg.go.dev/github.com/anacrolix/torrent)
|
||||||
|
|
||||||
|
This repository implements BitTorrent-related packages and command-line utilities in Go. The emphasis is on use as a library from other projects. It's been used 24/7 in production by downstream services since late 2014. The implementation was specifically created to explore Go's concurrency capabilities, and to include the ability to stream data directly from the BitTorrent network. To this end it [supports seeking, readaheads and other features](https://godoc.org/github.com/anacrolix/torrent#Reader) exposing torrents and their files with the various Go idiomatic `io` package interfaces. This is also demonstrated through [torrentfs](#torrentfs).
|
||||||
|
|
||||||
|
There is [support for protocol encryption, DHT, PEX, uTP, and various extensions](https://godoc.org/github.com/anacrolix/torrent). There are [several data storage backends provided](https://godoc.org/github.com/anacrolix/torrent/storage): blob, file, bolt, mmap, and sqlite, to name a few. You can [write your own](https://godoc.org/github.com/anacrolix/torrent/storage#ClientImpl) to store data for example on S3, or in a database.
|
||||||
|
|
||||||
|
Some noteworthy package dependencies that can be used for other purposes include:
|
||||||
|
|
||||||
|
* [go-libutp](https://github.com/anacrolix/go-libutp)
|
||||||
|
* [dht](https://github.com/anacrolix/dht)
|
||||||
|
* [bencode](https://godoc.org/github.com/anacrolix/torrent/bencode)
|
||||||
|
* [tracker](https://godoc.org/github.com/anacrolix/torrent/tracker)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install the library package with `go get github.com/anacrolix/torrent`, or the provided cmds with `go install github.com/anacrolix/torrent/cmd/...@latest`.
|
||||||
|
|
||||||
|
## Library examples
|
||||||
|
|
||||||
|
There are some small [examples](https://godoc.org/github.com/anacrolix/torrent#pkg-examples) in the package documentation.
|
||||||
|
|
||||||
|
## Mentions
|
||||||
|
|
||||||
|
* [@anacrolix](https://github.com/anacrolix) is interviewed about this repo in [Console 32](https://console.substack.com/p/console-32).
|
||||||
|
|
||||||
|
### Downstream projects
|
||||||
|
|
||||||
|
There are several web-frontends, sites, Android clients, storage backends and supporting services among the known public projects:
|
||||||
|
|
||||||
|
* [cove](https://coveapp.info): Personal torrent browser with streaming, DHT search, video transcoding and casting.
|
||||||
|
* [confluence](https://github.com/anacrolix/confluence): torrent client as a HTTP service <!-- Well of course I know him... He's me -->
|
||||||
|
* [Gopeed](https://github.com/GopeedLab/gopeed): Gopeed (full name Go Speed), a high-speed downloader developed by Golang + Flutter, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. <!-- 7.7k stars -->
|
||||||
|
* [Erigon](https://github.com/ledgerwatch/erigon): an implementation of Ethereum (execution layer with embeddable consensus layer), on the efficiency frontier. <!-- 2.7k stars -->
|
||||||
|
* [exatorrent](https://github.com/varbhat/exatorrent): Elegant self-hostable torrent client <!-- 1.5k stars -->
|
||||||
|
* [bitmagnet](https://github.com/bitmagnet-io/bitmagnet): A self-hosted BitTorrent indexer, DHT crawler, content classifier and torrent search engine with web UI, GraphQL API and Servarr stack integration. <!-- 1.1k stars -->
|
||||||
|
* [TorrServer](https://github.com/YouROK/TorrServer): Torrent streaming server over http <!-- 984 stars -->
|
||||||
|
* [distribyted](https://github.com/distribyted/distribyted): Distribyted is an alternative torrent client. It can expose torrent files as a standard FUSE, webDAV or HTTP endpoint and download them on demand, allowing random reads using a fixed amount of disk space. <!-- 982 stars -->
|
||||||
|
* [Simple Torrent](https://github.com/boypt/simple-torrent): self-hosted HTTP remote torrent client <!-- 876 stars -->
|
||||||
|
* [autobrr](https://github.com/autobrr/autobrr): autobrr redefines download automation for torrents and Usenet, drawing inspiration from tools like trackarr, autodl-irssi, and flexget. <!-- 855 stars -->
|
||||||
|
* [mabel](https://github.com/smmr-software/mabel): Fancy BitTorrent client for the terminal <!-- 412 stars -->
|
||||||
|
* [webtor.io](https://webtor.io/): free cloud BitTorrent-client <!-- not exclusively anacrolix/torrent maybe? 40-200 stars? -->
|
||||||
|
* [Android Torrent Client](https://gitlab.com/axet/android-torrent-client): Android torrent client <!-- 29 stars -->
|
||||||
|
* [libtorrent](https://gitlab.com/axet/libtorrent): gomobile wrapper <!-- 15 stars -->
|
||||||
|
* [Go-PeersToHTTP](https://github.com/WinPooh32/peerstohttp): Simple torrent proxy to http stream controlled over REST-like api <!-- 28 stars -->
|
||||||
|
* [CortexFoundation/torrentfs](https://github.com/CortexFoundation/torrentfs): Independent HTTP service for file seeding and P2P file system of cortex full node <!-- 21 stars -->
|
||||||
|
* [goTorrent](https://github.com/deranjer/goTorrent): torrenting server with a React web frontend <!-- 156 stars, inactive since 2020 -->
|
||||||
|
* [Go Peerflix](https://github.com/Sioro-Neoku/go-peerflix): Start watching the movie while your torrent is still downloading! <!-- 449 stars, inactive since 2019 -->
|
||||||
|
* [hTorrent](https://github.com/pojntfx/htorrent): HTTP to BitTorrent gateway with seeking support. <!-- 102 stars -->
|
||||||
|
* [Remote-Torrent](https://github.com/BruceWangNo1/remote-torrent): Download Remotely and Retrieve Files Over HTTP <!-- 57 stars, inactive since 2019 -->
|
||||||
|
* [Trickl](https://github.com/arranlomas/Trickl): torrent client for android <!-- 48 stars, inactive since 2018 -->
|
||||||
|
* [ANT-Downloader](https://github.com/anatasluo/ant): ANT Downloader is a BitTorrent Client developed by golang, angular 7, and electron <!-- archived -->
|
||||||
|
* [Elementum](http://elementum.surge.sh/) (up to version 0.0.71)
|
||||||
|
|
||||||
|
## Help
|
||||||
|
|
||||||
|
Communication about the project is primarily through [Discussions](https://github.com/anacrolix/torrent/discussions) and the [issue tracker](https://github.com/anacrolix/torrent/issues).
|
||||||
|
|
||||||
|
## Command packages
|
||||||
|
|
||||||
|
Here I'll describe what some of the packages in `./cmd` do. See [installation](#installation) to make them available.
|
||||||
|
|
||||||
|
### `torrent`
|
||||||
|
|
||||||
|
#### `torrent download`
|
||||||
|
|
||||||
|
Downloads torrents from the command-line.
|
||||||
|
|
||||||
|
$ torrent download 'magnet:?xt=urn:btih:KRWPCX3SJUM4IMM4YF5RPHL6ANPYTQPU'
|
||||||
|
... lots of jibber jabber ...
|
||||||
|
downloading "ubuntu-14.04.2-desktop-amd64.iso": 1.0 GB/1.0 GB, 1989/1992 pieces completed (1 partial)
|
||||||
|
2015/04/01 02:08:20 main.go:137: downloaded ALL the torrents
|
||||||
|
$ md5sum ubuntu-14.04.2-desktop-amd64.iso
|
||||||
|
1b305d585b1918f297164add46784116 ubuntu-14.04.2-desktop-amd64.iso
|
||||||
|
$ echo such amaze
|
||||||
|
wow
|
||||||
|
|
||||||
|
#### `torrent metainfo magnet`
|
||||||
|
|
||||||
|
Creates a magnet link from a torrent file. Note the extracted trackers, display name, and info hash.
|
||||||
|
|
||||||
|
$ torrent metainfo testdata/debian-10.8.0-amd64-netinst.iso.torrent magnet
|
||||||
|
magnet:?xt=urn:btih:4090c3c2a394a49974dfbbf2ce7ad0db3cdeddd7&dn=debian-10.8.0-amd64-netinst.iso&tr=http%3A%2F%2Fbttracker.debian.org%3A6969%2Fannounce
|
||||||
|
|
||||||
|
See `torrent metainfo --help` for other metainfo related commands.
|
||||||
|
|
||||||
|
### `torrentfs`
|
||||||
|
|
||||||
|
torrentfs mounts a FUSE filesystem at `-mountDir`. The contents are the torrents described by the torrent files and magnet links at `-metainfoDir`. Data for read requests is fetched only as required from the torrent network, and stored at `-downloadDir`.
|
||||||
|
|
||||||
|
$ mkdir mnt torrents
|
||||||
|
$ torrentfs -mountDir=mnt -metainfoDir=torrents &
|
||||||
|
$ cd torrents
|
||||||
|
$ wget http://releases.ubuntu.com/14.04.2/ubuntu-14.04.2-desktop-amd64.iso.torrent
|
||||||
|
$ cd ..
|
||||||
|
$ ls mnt
|
||||||
|
ubuntu-14.04.2-desktop-amd64.iso
|
||||||
|
$ pv mnt/ubuntu-14.04.2-desktop-amd64.iso | md5sum
|
||||||
|
996MB 0:04:40 [3.55MB/s] [========================================>] 100%
|
||||||
|
1b305d585b1918f297164add46784116 -
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
The two most recent minor releases are supported, with older versions receiving updates subject to contributor discretion.
|
||||||
|
Please also report issues in master, but there are no guarantees of stability there.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
All vulnerability reports are welcomed. Use your discretion in providing information to Discussions, an Issue, or message a maintainer directly.
|
||||||
|
For a non-trivial issue, you should receive a response within a week, but more than likely a day or two.
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
* Make use of sparse file regions in download data for faster hashing. This is available as whence 3 and 4 on some OSs?
|
||||||
|
* When we're choked and interested, are we not interested if there's no longer anything that we want?
|
||||||
|
* dht: Randomize triedAddrs bloom filter to allow different Addr sets on each Announce.
|
||||||
|
* data/blob: Deleting incomplete data triggers io.ErrUnexpectedEOF that isn't recovered from.
|
||||||
|
* Handle wanted pieces more efficiently, it's slow in in fillRequests, since the prioritization system was changed.
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
package analysis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/elliotchance/orderedmap"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
pp "github.com/anacrolix/torrent/peer_protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
type peerData struct {
|
||||||
|
requested *orderedmap.OrderedMap
|
||||||
|
haveDeleted map[torrent.Request]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracks the order that peers upload requests that we've sent them.
|
||||||
|
type PeerUploadOrder struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
peers map[*torrent.Peer]*peerData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *PeerUploadOrder) Init() {
|
||||||
|
me.peers = make(map[*torrent.Peer]*peerData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *PeerUploadOrder) onNewPeer(p *torrent.Peer) {
|
||||||
|
me.mu.Lock()
|
||||||
|
defer me.mu.Unlock()
|
||||||
|
if _, ok := me.peers[p]; ok {
|
||||||
|
panic("already have peer")
|
||||||
|
}
|
||||||
|
me.peers[p] = &peerData{
|
||||||
|
requested: orderedmap.NewOrderedMap(),
|
||||||
|
haveDeleted: make(map[torrent.Request]bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *PeerUploadOrder) onSentRequest(event torrent.PeerRequestEvent) {
|
||||||
|
me.mu.Lock()
|
||||||
|
defer me.mu.Unlock()
|
||||||
|
if !me.peers[event.Peer].requested.Set(event.Request, nil) {
|
||||||
|
panic("duplicate request sent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *PeerUploadOrder) Install(cbs *torrent.Callbacks) {
|
||||||
|
cbs.NewPeer = append(cbs.NewPeer, me.onNewPeer)
|
||||||
|
cbs.SentRequest = append(cbs.SentRequest, me.onSentRequest)
|
||||||
|
cbs.ReceivedRequested = append(cbs.ReceivedRequested, me.onReceivedRequested)
|
||||||
|
cbs.DeletedRequest = append(cbs.DeletedRequest, me.deletedRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *PeerUploadOrder) report(desc string, req torrent.Request, peer *torrent.Peer) {
|
||||||
|
peerConn, ok := peer.TryAsPeerConn()
|
||||||
|
var peerId *torrent.PeerID
|
||||||
|
if ok {
|
||||||
|
peerId = &peerConn.PeerID
|
||||||
|
}
|
||||||
|
log.Printf("%s: %v, %v", desc, req, peerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *PeerUploadOrder) onReceivedRequested(event torrent.PeerMessageEvent) {
|
||||||
|
req := torrent.Request{
|
||||||
|
event.Message.Index,
|
||||||
|
torrent.ChunkSpec{
|
||||||
|
Begin: event.Message.Begin,
|
||||||
|
Length: pp.Integer(len(event.Message.Piece)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
makeLogMsg := func(desc string) string {
|
||||||
|
peerConn, ok := event.Peer.TryAsPeerConn()
|
||||||
|
var peerId *torrent.PeerID
|
||||||
|
if ok {
|
||||||
|
peerId = &peerConn.PeerID
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s: %q, %v", desc, peerId, req)
|
||||||
|
}
|
||||||
|
me.mu.Lock()
|
||||||
|
defer me.mu.Unlock()
|
||||||
|
peerData := me.peers[event.Peer]
|
||||||
|
if peerData.requested.Front().Key.(torrent.Request) == req {
|
||||||
|
log.Print(makeLogMsg("got next requested piece"))
|
||||||
|
} else if _, ok := peerData.requested.Get(req); ok {
|
||||||
|
log.Print(makeLogMsg(fmt.Sprintf(
|
||||||
|
"got requested piece but not next (previous delete=%v)",
|
||||||
|
peerData.haveDeleted[req])))
|
||||||
|
} else {
|
||||||
|
panic(makeLogMsg("got unrequested piece"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *PeerUploadOrder) deletedRequest(event torrent.PeerRequestEvent) {
|
||||||
|
me.mu.Lock()
|
||||||
|
defer me.mu.Unlock()
|
||||||
|
peerData := me.peers[event.Peer]
|
||||||
|
if !peerData.requested.Delete(event.Request) {
|
||||||
|
panic("nothing to delete")
|
||||||
|
}
|
||||||
|
peerData.haveDeleted[event.Request] = true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/internal/testutil"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type badStorage struct{}
|
||||||
|
|
||||||
|
var _ storage.ClientImpl = badStorage{}
|
||||||
|
|
||||||
|
func (bs badStorage) OpenTorrent(*metainfo.Info, metainfo.Hash) (storage.TorrentImpl, error) {
|
||||||
|
return storage.TorrentImpl{
|
||||||
|
Piece: bs.Piece,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs badStorage) Piece(p metainfo.Piece) storage.PieceImpl {
|
||||||
|
return badStoragePiece{p}
|
||||||
|
}
|
||||||
|
|
||||||
|
type badStoragePiece struct {
|
||||||
|
p metainfo.Piece
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ storage.PieceImpl = badStoragePiece{}
|
||||||
|
|
||||||
|
func (p badStoragePiece) WriteAt(b []byte, off int64) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p badStoragePiece) Completion() storage.Completion {
|
||||||
|
return storage.Completion{Complete: true, Ok: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p badStoragePiece) MarkComplete() error {
|
||||||
|
return errors.New("psyyyyyyyche")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p badStoragePiece) MarkNotComplete() error {
|
||||||
|
return errors.New("psyyyyyyyche")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p badStoragePiece) randomlyTruncatedDataString() string {
|
||||||
|
return testutil.GreetingFileContents[:rand.Intn(14)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p badStoragePiece) ReadAt(b []byte, off int64) (n int, err error) {
|
||||||
|
r := strings.NewReader(p.randomlyTruncatedDataString())
|
||||||
|
return r.ReadAt(b, off+p.p.Offset())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
Bencode encoding/decoding sub package. Uses similar API design to Go's json package.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get github.com/anacrolix/torrent
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package demo
|
||||||
|
|
||||||
|
import (
|
||||||
|
bencode "github.com/anacrolix/torrent/bencode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Query string `json:"q,omitempty" bencode:"q,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var v Message
|
||||||
|
|
||||||
|
func main(){
|
||||||
|
// encode
|
||||||
|
data, err := bencode.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//decode
|
||||||
|
err := bencode.Unmarshal(data, &v)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println(v)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
package bencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/anacrolix/missinggo/expect"
|
||||||
|
)
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
// Errors
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// In case if marshaler cannot encode a type, it will return this error. Typical
|
||||||
|
// example of such type is float32/float64 which has no bencode representation.
|
||||||
|
type MarshalTypeError struct {
|
||||||
|
Type reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MarshalTypeError) Error() string {
|
||||||
|
return "bencode: unsupported type: " + e.Type.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal argument must be a non-nil value of some pointer type.
|
||||||
|
type UnmarshalInvalidArgError struct {
|
||||||
|
Type reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UnmarshalInvalidArgError) Error() string {
|
||||||
|
if e.Type == nil {
|
||||||
|
return "bencode: Unmarshal(nil)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Type.Kind() != reflect.Ptr {
|
||||||
|
return "bencode: Unmarshal(non-pointer " + e.Type.String() + ")"
|
||||||
|
}
|
||||||
|
return "bencode: Unmarshal(nil " + e.Type.String() + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshaler spotted a value that was not appropriate for a given Go value.
|
||||||
|
type UnmarshalTypeError struct {
|
||||||
|
BencodeTypeName string
|
||||||
|
UnmarshalTargetType reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
// This could probably be a value type, but we may already have users assuming
|
||||||
|
// that it's passed by pointer.
|
||||||
|
func (e *UnmarshalTypeError) Error() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"can't unmarshal a bencode %v into a %v",
|
||||||
|
e.BencodeTypeName,
|
||||||
|
e.UnmarshalTargetType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshaler tried to write to an unexported (therefore unwritable) field.
|
||||||
|
type UnmarshalFieldError struct {
|
||||||
|
Key string
|
||||||
|
Type reflect.Type
|
||||||
|
Field reflect.StructField
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UnmarshalFieldError) Error() string {
|
||||||
|
return "bencode: key \"" + e.Key + "\" led to an unexported field \"" +
|
||||||
|
e.Field.Name + "\" in type: " + e.Type.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Malformed bencode input, unmarshaler failed to parse it.
|
||||||
|
type SyntaxError struct {
|
||||||
|
Offset int64 // location of the error
|
||||||
|
What error // error description
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SyntaxError) Error() string {
|
||||||
|
return fmt.Sprintf("bencode: syntax error (offset: %d): %s", e.Offset, e.What)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A non-nil error was returned after calling MarshalBencode on a type which
|
||||||
|
// implements the Marshaler interface.
|
||||||
|
type MarshalerError struct {
|
||||||
|
Type reflect.Type
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MarshalerError) Error() string {
|
||||||
|
return "bencode: error calling MarshalBencode for type " + e.Type.String() + ": " + e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// A non-nil error was returned after calling UnmarshalBencode on a type which
|
||||||
|
// implements the Unmarshaler interface.
|
||||||
|
type UnmarshalerError struct {
|
||||||
|
Type reflect.Type
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UnmarshalerError) Error() string {
|
||||||
|
return "bencode: error calling UnmarshalBencode for type " + e.Type.String() + ": " + e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
// Interfaces
|
||||||
|
//----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Any type which implements this interface, will be marshaled using the
|
||||||
|
// specified method.
|
||||||
|
type Marshaler interface {
|
||||||
|
MarshalBencode() ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any type which implements this interface, will be unmarshaled using the
|
||||||
|
// specified method.
|
||||||
|
type Unmarshaler interface {
|
||||||
|
UnmarshalBencode([]byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal the value 'v' to the bencode form, return the result as []byte and
|
||||||
|
// an error if any.
|
||||||
|
func Marshal(v interface{}) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
e := Encoder{w: &buf}
|
||||||
|
err := e.Encode(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustMarshal(v interface{}) []byte {
|
||||||
|
b, err := Marshal(v)
|
||||||
|
expect.Nil(err)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal the bencode value in the 'data' to a value pointed by the 'v' pointer, return a non-nil
|
||||||
|
// error if any. If there are trailing bytes, this results in ErrUnusedTrailingBytes, but the value
|
||||||
|
// will be valid. It's probably more consistent to use Decoder.Decode if you want to rely on this
|
||||||
|
// behaviour (inspired by Rust's serde here).
|
||||||
|
func Unmarshal(data []byte, v interface{}) (err error) {
|
||||||
|
buf := bytes.NewReader(data)
|
||||||
|
e := Decoder{r: buf}
|
||||||
|
err = e.Decode(v)
|
||||||
|
if err == nil && buf.Len() != 0 {
|
||||||
|
err = ErrUnusedTrailingBytes{buf.Len()}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrUnusedTrailingBytes struct {
|
||||||
|
NumUnusedBytes int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me ErrUnusedTrailingBytes) Error() string {
|
||||||
|
return fmt.Sprintf("%d unused trailing bytes", me.NumUnusedBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDecoder(r io.Reader) *Decoder {
|
||||||
|
return &Decoder{r: &scanner{r: r}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEncoder(w io.Writer) *Encoder {
|
||||||
|
return &Encoder{w: w}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package bencode_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anacrolix/dht/v2/krpc"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/bencode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func marshalAndUnmarshal(tb testing.TB, orig krpc.Msg) (ret krpc.Msg) {
|
||||||
|
b, err := bencode.Marshal(orig)
|
||||||
|
if err != nil {
|
||||||
|
tb.Fatal(err)
|
||||||
|
}
|
||||||
|
err = bencode.Unmarshal(b, &ret)
|
||||||
|
if err != nil {
|
||||||
|
tb.Fatal(err)
|
||||||
|
}
|
||||||
|
// ret.Q = "what"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMarshalThenUnmarshalKrpcMsg(tb *testing.B) {
|
||||||
|
orig := krpc.Msg{
|
||||||
|
T: "420",
|
||||||
|
Y: "r",
|
||||||
|
R: &krpc.Return{
|
||||||
|
Token: func() *string { t := "re-up"; return &t }(),
|
||||||
|
},
|
||||||
|
IP: krpc.NodeAddr{IP: net.ParseIP("1.2.3.4"), Port: 1337},
|
||||||
|
ReadOnly: true,
|
||||||
|
}
|
||||||
|
first := marshalAndUnmarshal(tb, orig)
|
||||||
|
if !reflect.DeepEqual(orig, first) {
|
||||||
|
tb.Fail()
|
||||||
|
}
|
||||||
|
tb.ReportAllocs()
|
||||||
|
tb.ResetTimer()
|
||||||
|
for i := 0; i < tb.N; i += 1 {
|
||||||
|
marshalAndUnmarshal(tb, orig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package bencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadFile(name string, t *testing.T) []byte {
|
||||||
|
data, err := os.ReadFile(name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFileInterface(t *testing.T, filename string) {
|
||||||
|
data1 := loadFile(filename, t)
|
||||||
|
|
||||||
|
var iface interface{}
|
||||||
|
err := Unmarshal(data1, &iface)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data2, err := Marshal(iface)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, data1, data2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBothInterface(t *testing.T) {
|
||||||
|
testFileInterface(t, "testdata/archlinux-2011.08.19-netinstall-i686.iso.torrent")
|
||||||
|
testFileInterface(t, "testdata/continuum.torrent")
|
||||||
|
}
|
||||||
|
|
||||||
|
type torrentFile struct {
|
||||||
|
Info struct {
|
||||||
|
Name string `bencode:"name"`
|
||||||
|
Length int64 `bencode:"length"`
|
||||||
|
MD5Sum string `bencode:"md5sum,omitempty"`
|
||||||
|
PieceLength int64 `bencode:"piece length"`
|
||||||
|
Pieces string `bencode:"pieces"`
|
||||||
|
Private bool `bencode:"private,omitempty"`
|
||||||
|
} `bencode:"info"`
|
||||||
|
|
||||||
|
Announce string `bencode:"announce"`
|
||||||
|
AnnounceList [][]string `bencode:"announce-list,omitempty"`
|
||||||
|
CreationDate int64 `bencode:"creation date,omitempty"`
|
||||||
|
Comment string `bencode:"comment,omitempty"`
|
||||||
|
CreatedBy string `bencode:"created by,omitempty"`
|
||||||
|
URLList interface{} `bencode:"url-list,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFile(t *testing.T, filename string) {
|
||||||
|
data1 := loadFile(filename, t)
|
||||||
|
var f torrentFile
|
||||||
|
|
||||||
|
err := Unmarshal(data1, &f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data2, err := Marshal(&f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(data1, data2) {
|
||||||
|
println(string(data2))
|
||||||
|
t.Fatalf("equality expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoth(t *testing.T) {
|
||||||
|
testFile(t, "testdata/archlinux-2011.08.19-netinstall-i686.iso.torrent")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package bencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bytes []byte
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ Unmarshaler = (*Bytes)(nil)
|
||||||
|
_ Marshaler = (*Bytes)(nil)
|
||||||
|
_ Marshaler = Bytes{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (me *Bytes) UnmarshalBencode(b []byte) error {
|
||||||
|
*me = append([]byte(nil), b...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me Bytes) MarshalBencode() ([]byte, error) {
|
||||||
|
if len(me) == 0 {
|
||||||
|
return nil, errors.New("marshalled Bytes should not be zero-length")
|
||||||
|
}
|
||||||
|
return me, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me Bytes) GoString() string {
|
||||||
|
return fmt.Sprintf("bencode.Bytes(%q)", []byte(me))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package bencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
qt "github.com/frankban/quicktest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBytesMarshalNil(t *testing.T) {
|
||||||
|
var b Bytes
|
||||||
|
Marshal(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
type structWithBytes struct {
|
||||||
|
A Bytes
|
||||||
|
B Bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalNilStructBytes(t *testing.T) {
|
||||||
|
_, err := Marshal(structWithBytes{B: Bytes("i42e")})
|
||||||
|
c := qt.New(t)
|
||||||
|
c.Assert(err, qt.IsNotNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
type structWithOmitEmptyBytes struct {
|
||||||
|
A Bytes `bencode:",omitempty"`
|
||||||
|
B Bytes `bencode:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalNilStructBytesOmitEmpty(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
b, err := Marshal(structWithOmitEmptyBytes{B: Bytes("i42e")})
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
t.Logf("%q", b)
|
||||||
|
var s structWithBytes
|
||||||
|
err = Unmarshal(b, &s)
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
c.Check(s.B, qt.DeepEquals, Bytes("i42e"))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,752 @@
|
||||||
|
package bencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The default bencode string length limit. This is a poor attempt to prevent excessive memory
|
||||||
|
// allocation when parsing, but also leaves the window open to implement a better solution.
|
||||||
|
const DefaultDecodeMaxStrLen = 1<<27 - 1 // ~128MiB
|
||||||
|
|
||||||
|
type MaxStrLen = int64
|
||||||
|
|
||||||
|
type Decoder struct {
|
||||||
|
// Maximum parsed bencode string length. Defaults to DefaultMaxStrLen if zero.
|
||||||
|
MaxStrLen MaxStrLen
|
||||||
|
|
||||||
|
r interface {
|
||||||
|
io.ByteScanner
|
||||||
|
io.Reader
|
||||||
|
}
|
||||||
|
// Sum of bytes used to Decode values.
|
||||||
|
Offset int64
|
||||||
|
buf bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) Decode(v interface{}) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r := recover()
|
||||||
|
if r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, ok := r.(runtime.Error)
|
||||||
|
if ok {
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
if err, ok = r.(error); !ok {
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
// Errors thrown from deeper in parsing are unexpected. At value boundaries, errors should
|
||||||
|
// be returned directly (at least until all the panic nonsense is removed entirely).
|
||||||
|
if err == io.EOF {
|
||||||
|
err = io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
pv := reflect.ValueOf(v)
|
||||||
|
if pv.Kind() != reflect.Ptr || pv.IsNil() {
|
||||||
|
return &UnmarshalInvalidArgError{reflect.TypeOf(v)}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := d.parseValue(pv.Elem())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
d.throwSyntaxError(d.Offset-1, errors.New("unexpected 'e'"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkForUnexpectedEOF(err error, offset int64) {
|
||||||
|
if err == io.EOF {
|
||||||
|
panic(&SyntaxError{
|
||||||
|
Offset: offset,
|
||||||
|
What: io.ErrUnexpectedEOF,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) readByte() byte {
|
||||||
|
b, err := d.r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
checkForUnexpectedEOF(err, d.Offset)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Offset++
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// reads data writing it to 'd.buf' until 'sep' byte is encountered, 'sep' byte
|
||||||
|
// is consumed, but not included into the 'd.buf'
|
||||||
|
func (d *Decoder) readUntil(sep byte) {
|
||||||
|
for {
|
||||||
|
b := d.readByte()
|
||||||
|
if b == sep {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.buf.WriteByte(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkForIntParseError(err error, offset int64) {
|
||||||
|
if err != nil {
|
||||||
|
panic(&SyntaxError{
|
||||||
|
Offset: offset,
|
||||||
|
What: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) throwSyntaxError(offset int64, err error) {
|
||||||
|
panic(&SyntaxError{
|
||||||
|
Offset: offset,
|
||||||
|
What: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume the 'i' is already consumed. Read and validate the rest of an int into the buffer.
|
||||||
|
func (d *Decoder) readInt() error {
|
||||||
|
// start := d.Offset - 1
|
||||||
|
d.readUntil('e')
|
||||||
|
if err := d.checkBufferedInt(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// if d.buf.Len() == 0 {
|
||||||
|
// panic(&SyntaxError{
|
||||||
|
// Offset: start,
|
||||||
|
// What: errors.New("empty integer value"),
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// called when 'i' was consumed, for the integer type in v.
|
||||||
|
func (d *Decoder) parseInt(v reflect.Value) error {
|
||||||
|
start := d.Offset - 1
|
||||||
|
|
||||||
|
if err := d.readInt(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s := bytesAsString(d.buf.Bytes())
|
||||||
|
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
n, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
checkForIntParseError(err, start)
|
||||||
|
|
||||||
|
if v.OverflowInt(n) {
|
||||||
|
return &UnmarshalTypeError{
|
||||||
|
BencodeTypeName: "int",
|
||||||
|
UnmarshalTargetType: v.Type(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.SetInt(n)
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
n, err := strconv.ParseUint(s, 10, 64)
|
||||||
|
checkForIntParseError(err, start)
|
||||||
|
|
||||||
|
if v.OverflowUint(n) {
|
||||||
|
return &UnmarshalTypeError{
|
||||||
|
BencodeTypeName: "int",
|
||||||
|
UnmarshalTargetType: v.Type(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.SetUint(n)
|
||||||
|
case reflect.Bool:
|
||||||
|
v.SetBool(s != "0")
|
||||||
|
default:
|
||||||
|
return &UnmarshalTypeError{
|
||||||
|
BencodeTypeName: "int",
|
||||||
|
UnmarshalTargetType: v.Type(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.buf.Reset()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) checkBufferedInt() error {
|
||||||
|
b := d.buf.Bytes()
|
||||||
|
if len(b) <= 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if b[0] == '-' {
|
||||||
|
b = b[1:]
|
||||||
|
}
|
||||||
|
if b[0] < '1' || b[0] > '9' {
|
||||||
|
return errors.New("invalid leading digit")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) parseStringLength() (int, error) {
|
||||||
|
// We should have already consumed the first byte of the length into the Decoder buf.
|
||||||
|
start := d.Offset - 1
|
||||||
|
d.readUntil(':')
|
||||||
|
if err := d.checkBufferedInt(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
// Really the limit should be the uint size for the platform. But we can't pass in an allocator,
|
||||||
|
// or limit total memory use in Go, the best we might hope to do is limit the size of a single
|
||||||
|
// decoded value (by reading it in in-place and then operating on a view).
|
||||||
|
length, err := strconv.ParseInt(bytesAsString(d.buf.Bytes()), 10, 0)
|
||||||
|
checkForIntParseError(err, start)
|
||||||
|
if int64(length) > d.getMaxStrLen() {
|
||||||
|
err = fmt.Errorf("parsed string length %v exceeds limit (%v)", length, DefaultDecodeMaxStrLen)
|
||||||
|
}
|
||||||
|
d.buf.Reset()
|
||||||
|
return int(length), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) parseString(v reflect.Value) error {
|
||||||
|
length, err := d.parseStringLength()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer d.buf.Reset()
|
||||||
|
read := func(b []byte) {
|
||||||
|
n, err := io.ReadFull(d.r, b)
|
||||||
|
d.Offset += int64(n)
|
||||||
|
if err != nil {
|
||||||
|
checkForUnexpectedEOF(err, d.Offset)
|
||||||
|
panic(&SyntaxError{
|
||||||
|
Offset: d.Offset,
|
||||||
|
What: errors.New("unexpected I/O error: " + err.Error()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
b := make([]byte, length)
|
||||||
|
read(b)
|
||||||
|
v.SetString(bytesAsString(b))
|
||||||
|
return nil
|
||||||
|
case reflect.Slice:
|
||||||
|
if v.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b := make([]byte, length)
|
||||||
|
read(b)
|
||||||
|
v.SetBytes(b)
|
||||||
|
return nil
|
||||||
|
case reflect.Array:
|
||||||
|
if v.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
d.buf.Grow(length)
|
||||||
|
b := d.buf.Bytes()[:length]
|
||||||
|
read(b)
|
||||||
|
reflect.Copy(v, reflect.ValueOf(b))
|
||||||
|
return nil
|
||||||
|
case reflect.Bool:
|
||||||
|
d.buf.Grow(length)
|
||||||
|
b := d.buf.Bytes()[:length]
|
||||||
|
read(b)
|
||||||
|
x, err := strconv.ParseBool(bytesAsString(b))
|
||||||
|
if err != nil {
|
||||||
|
x = length != 0
|
||||||
|
}
|
||||||
|
v.SetBool(x)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Can't move this into default clause because some cases above fail through to here after
|
||||||
|
// additional checks.
|
||||||
|
d.buf.Grow(length)
|
||||||
|
read(d.buf.Bytes()[:length])
|
||||||
|
// I believe we return here to support "ignore_unmarshal_type_error".
|
||||||
|
return &UnmarshalTypeError{
|
||||||
|
BencodeTypeName: "string",
|
||||||
|
UnmarshalTargetType: v.Type(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info for parsing a dict value.
|
||||||
|
type dictField struct {
|
||||||
|
Type reflect.Type
|
||||||
|
Get func(value reflect.Value) func(reflect.Value)
|
||||||
|
Tags tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns specifics for parsing a dict field value.
|
||||||
|
func getDictField(dict reflect.Type, key string) (_ dictField, err error) {
|
||||||
|
// get valuev as a map value or as a struct field
|
||||||
|
switch k := dict.Kind(); k {
|
||||||
|
case reflect.Map:
|
||||||
|
return dictField{
|
||||||
|
Type: dict.Elem(),
|
||||||
|
Get: func(mapValue reflect.Value) func(reflect.Value) {
|
||||||
|
return func(value reflect.Value) {
|
||||||
|
if mapValue.IsNil() {
|
||||||
|
mapValue.Set(reflect.MakeMap(dict))
|
||||||
|
}
|
||||||
|
// Assigns the value into the map.
|
||||||
|
// log.Printf("map type: %v", mapValue.Type())
|
||||||
|
mapValue.SetMapIndex(reflect.ValueOf(key).Convert(dict.Key()), value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
case reflect.Struct:
|
||||||
|
return getStructFieldForKey(dict, key), nil
|
||||||
|
// if sf.r.PkgPath != "" {
|
||||||
|
// panic(&UnmarshalFieldError{
|
||||||
|
// Key: key,
|
||||||
|
// Type: dict.Type(),
|
||||||
|
// Field: sf.r,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("can't assign bencode dict items into a %v", k)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
structFieldsMu sync.Mutex
|
||||||
|
structFields = map[reflect.Type]map[string]dictField{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseStructFields(struct_ reflect.Type, each func(key string, df dictField)) {
|
||||||
|
for _i, n := 0, struct_.NumField(); _i < n; _i++ {
|
||||||
|
i := _i
|
||||||
|
f := struct_.Field(i)
|
||||||
|
if f.Anonymous {
|
||||||
|
t := f.Type
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
parseStructFields(t, func(key string, df dictField) {
|
||||||
|
innerGet := df.Get
|
||||||
|
df.Get = func(value reflect.Value) func(reflect.Value) {
|
||||||
|
anonPtr := value.Field(i)
|
||||||
|
if anonPtr.Kind() == reflect.Ptr && anonPtr.IsNil() {
|
||||||
|
anonPtr.Set(reflect.New(f.Type.Elem()))
|
||||||
|
anonPtr = anonPtr.Elem()
|
||||||
|
}
|
||||||
|
return innerGet(anonPtr)
|
||||||
|
}
|
||||||
|
each(key, df)
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tagStr := f.Tag.Get("bencode")
|
||||||
|
if tagStr == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tag := parseTag(tagStr)
|
||||||
|
key := tag.Key()
|
||||||
|
if key == "" {
|
||||||
|
key = f.Name
|
||||||
|
}
|
||||||
|
each(key, dictField{f.Type, func(value reflect.Value) func(reflect.Value) {
|
||||||
|
return value.Field(i).Set
|
||||||
|
}, tag})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveStructFields(struct_ reflect.Type) {
|
||||||
|
m := make(map[string]dictField)
|
||||||
|
parseStructFields(struct_, func(key string, sf dictField) {
|
||||||
|
m[key] = sf
|
||||||
|
})
|
||||||
|
structFields[struct_] = m
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStructFieldForKey(struct_ reflect.Type, key string) (f dictField) {
|
||||||
|
structFieldsMu.Lock()
|
||||||
|
if _, ok := structFields[struct_]; !ok {
|
||||||
|
saveStructFields(struct_)
|
||||||
|
}
|
||||||
|
f, ok := structFields[struct_][key]
|
||||||
|
structFieldsMu.Unlock()
|
||||||
|
if !ok {
|
||||||
|
var discard interface{}
|
||||||
|
return dictField{
|
||||||
|
Type: reflect.TypeOf(discard),
|
||||||
|
Get: func(reflect.Value) func(reflect.Value) { return func(reflect.Value) {} },
|
||||||
|
Tags: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) parseDict(v reflect.Value) error {
|
||||||
|
// At this point 'd' byte was consumed, now read key/value pairs
|
||||||
|
for {
|
||||||
|
var keyStr string
|
||||||
|
keyValue := reflect.ValueOf(&keyStr).Elem()
|
||||||
|
ok, err := d.parseValue(keyValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing dict key: %w", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
df, err := getDictField(v.Type(), keyStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing bencode dict into %v: %w", v.Type(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// now we need to actually parse it
|
||||||
|
if df.Type == nil {
|
||||||
|
// Discard the value, there's nowhere to put it.
|
||||||
|
var if_ interface{}
|
||||||
|
if_, ok = d.parseValueInterface()
|
||||||
|
if if_ == nil {
|
||||||
|
return fmt.Errorf("error parsing value for key %q", keyStr)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("missing value for key %q", keyStr)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
setValue := reflect.New(df.Type).Elem()
|
||||||
|
// log.Printf("parsing into %v", setValue.Type())
|
||||||
|
ok, err = d.parseValue(setValue)
|
||||||
|
if err != nil {
|
||||||
|
var target *UnmarshalTypeError
|
||||||
|
if !(errors.As(err, &target) && df.Tags.IgnoreUnmarshalTypeError()) {
|
||||||
|
return fmt.Errorf("parsing value for key %q: %w", keyStr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("missing value for key %q", keyStr)
|
||||||
|
}
|
||||||
|
df.Get(v)(setValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) parseList(v reflect.Value) error {
|
||||||
|
switch v.Kind() {
|
||||||
|
default:
|
||||||
|
// If the list is a singleton of the expected type, use that value. See
|
||||||
|
// https://github.com/anacrolix/torrent/issues/297.
|
||||||
|
l := reflect.New(reflect.SliceOf(v.Type()))
|
||||||
|
if err := d.parseList(l.Elem()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if l.Elem().Len() != 1 {
|
||||||
|
return &UnmarshalTypeError{
|
||||||
|
BencodeTypeName: "list",
|
||||||
|
UnmarshalTargetType: v.Type(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.Set(l.Elem().Index(0))
|
||||||
|
return nil
|
||||||
|
case reflect.Array, reflect.Slice:
|
||||||
|
// We can work with this. Normal case, fallthrough.
|
||||||
|
}
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for ; ; i++ {
|
||||||
|
if v.Kind() == reflect.Slice && i >= v.Len() {
|
||||||
|
v.Set(reflect.Append(v, reflect.Zero(v.Type().Elem())))
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < v.Len() {
|
||||||
|
ok, err := d.parseValue(v.Index(i))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, ok := d.parseValueInterface()
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < v.Len() {
|
||||||
|
if v.Kind() == reflect.Array {
|
||||||
|
z := reflect.Zero(v.Type().Elem())
|
||||||
|
for n := v.Len(); i < n; i++ {
|
||||||
|
v.Index(i).Set(z)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v.SetLen(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 0 && v.Kind() == reflect.Slice {
|
||||||
|
v.Set(reflect.MakeSlice(v.Type(), 0, 0))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) readOneValue() bool {
|
||||||
|
b, err := d.r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if b == 'e' {
|
||||||
|
d.r.UnreadByte()
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
d.Offset++
|
||||||
|
d.buf.WriteByte(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch b {
|
||||||
|
case 'd', 'l':
|
||||||
|
// read until there is nothing to read
|
||||||
|
for d.readOneValue() {
|
||||||
|
}
|
||||||
|
// consume 'e' as well
|
||||||
|
b = d.readByte()
|
||||||
|
d.buf.WriteByte(b)
|
||||||
|
case 'i':
|
||||||
|
d.readUntil('e')
|
||||||
|
d.buf.WriteString("e")
|
||||||
|
default:
|
||||||
|
if b >= '0' && b <= '9' {
|
||||||
|
start := d.buf.Len() - 1
|
||||||
|
d.readUntil(':')
|
||||||
|
length, err := strconv.ParseInt(bytesAsString(d.buf.Bytes()[start:]), 10, 64)
|
||||||
|
checkForIntParseError(err, d.Offset-1)
|
||||||
|
|
||||||
|
d.buf.WriteString(":")
|
||||||
|
n, err := io.CopyN(&d.buf, d.r, length)
|
||||||
|
d.Offset += n
|
||||||
|
if err != nil {
|
||||||
|
checkForUnexpectedEOF(err, d.Offset)
|
||||||
|
panic(&SyntaxError{
|
||||||
|
Offset: d.Offset,
|
||||||
|
What: errors.New("unexpected I/O error: " + err.Error()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
d.raiseUnknownValueType(b, d.Offset-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) parseUnmarshaler(v reflect.Value) bool {
|
||||||
|
if !v.Type().Implements(unmarshalerType) {
|
||||||
|
if v.Addr().Type().Implements(unmarshalerType) {
|
||||||
|
v = v.Addr()
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.buf.Reset()
|
||||||
|
if !d.readOneValue() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
m := v.Interface().(Unmarshaler)
|
||||||
|
err := m.UnmarshalBencode(d.buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
panic(&UnmarshalerError{v.Type(), err})
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if there was a value and it's now stored in 'v', otherwise
|
||||||
|
// there was an end symbol ("e") and no value was stored.
|
||||||
|
func (d *Decoder) parseValue(v reflect.Value) (bool, error) {
|
||||||
|
// we support one level of indirection at the moment
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
// if the pointer is nil, allocate a new element of the type it
|
||||||
|
// points to
|
||||||
|
if v.IsNil() {
|
||||||
|
v.Set(reflect.New(v.Type().Elem()))
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.parseUnmarshaler(v) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// common case: interface{}
|
||||||
|
if v.Kind() == reflect.Interface && v.NumMethod() == 0 {
|
||||||
|
iface, _ := d.parseValueInterface()
|
||||||
|
v.Set(reflect.ValueOf(iface))
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := d.r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
d.Offset++
|
||||||
|
|
||||||
|
switch b {
|
||||||
|
case 'e':
|
||||||
|
return false, nil
|
||||||
|
case 'd':
|
||||||
|
return true, d.parseDict(v)
|
||||||
|
case 'l':
|
||||||
|
return true, d.parseList(v)
|
||||||
|
case 'i':
|
||||||
|
return true, d.parseInt(v)
|
||||||
|
default:
|
||||||
|
if b >= '0' && b <= '9' {
|
||||||
|
// It's a string.
|
||||||
|
d.buf.Reset()
|
||||||
|
// Write the first digit of the length to the buffer.
|
||||||
|
d.buf.WriteByte(b)
|
||||||
|
return true, d.parseString(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.raiseUnknownValueType(b, d.Offset-1)
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// An unknown bencode type character was encountered.
|
||||||
|
func (d *Decoder) raiseUnknownValueType(b byte, offset int64) {
|
||||||
|
panic(&SyntaxError{
|
||||||
|
Offset: offset,
|
||||||
|
What: fmt.Errorf("unknown value type %+q", b),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) parseValueInterface() (interface{}, bool) {
|
||||||
|
b, err := d.r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
d.Offset++
|
||||||
|
|
||||||
|
switch b {
|
||||||
|
case 'e':
|
||||||
|
return nil, false
|
||||||
|
case 'd':
|
||||||
|
return d.parseDictInterface(), true
|
||||||
|
case 'l':
|
||||||
|
return d.parseListInterface(), true
|
||||||
|
case 'i':
|
||||||
|
return d.parseIntInterface(), true
|
||||||
|
default:
|
||||||
|
if b >= '0' && b <= '9' {
|
||||||
|
// string
|
||||||
|
// append first digit of the length to the buffer
|
||||||
|
d.buf.WriteByte(b)
|
||||||
|
return d.parseStringInterface(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
d.raiseUnknownValueType(b, d.Offset-1)
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called after 'i', for an arbitrary integer size.
|
||||||
|
func (d *Decoder) parseIntInterface() (ret interface{}) {
|
||||||
|
start := d.Offset - 1
|
||||||
|
|
||||||
|
if err := d.readInt(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
n, err := strconv.ParseInt(d.buf.String(), 10, 64)
|
||||||
|
if ne, ok := err.(*strconv.NumError); ok && ne.Err == strconv.ErrRange {
|
||||||
|
i := new(big.Int)
|
||||||
|
_, ok := i.SetString(d.buf.String(), 10)
|
||||||
|
if !ok {
|
||||||
|
panic(&SyntaxError{
|
||||||
|
Offset: start,
|
||||||
|
What: errors.New("failed to parse integer"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ret = i
|
||||||
|
} else {
|
||||||
|
checkForIntParseError(err, start)
|
||||||
|
ret = n
|
||||||
|
}
|
||||||
|
|
||||||
|
d.buf.Reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) readBytes(length int) []byte {
|
||||||
|
b, err := io.ReadAll(io.LimitReader(d.r, int64(length)))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if len(b) != length {
|
||||||
|
panic(fmt.Errorf("read %v bytes expected %v", len(b), length))
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) parseStringInterface() string {
|
||||||
|
length, err := d.parseStringLength()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
b := d.readBytes(int(length))
|
||||||
|
d.Offset += int64(len(b))
|
||||||
|
if err != nil {
|
||||||
|
panic(&SyntaxError{Offset: d.Offset, What: err})
|
||||||
|
}
|
||||||
|
return bytesAsString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) parseDictInterface() interface{} {
|
||||||
|
dict := make(map[string]interface{})
|
||||||
|
var lastKey string
|
||||||
|
lastKeyOk := false
|
||||||
|
for {
|
||||||
|
start := d.Offset
|
||||||
|
keyi, ok := d.parseValueInterface()
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
key, ok := keyi.(string)
|
||||||
|
if !ok {
|
||||||
|
panic(&SyntaxError{
|
||||||
|
Offset: d.Offset,
|
||||||
|
What: errors.New("non-string key in a dict"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if lastKeyOk && key <= lastKey {
|
||||||
|
d.throwSyntaxError(start, fmt.Errorf("dict keys unsorted: %q <= %q", key, lastKey))
|
||||||
|
}
|
||||||
|
start = d.Offset
|
||||||
|
valuei, ok := d.parseValueInterface()
|
||||||
|
if !ok {
|
||||||
|
d.throwSyntaxError(start, fmt.Errorf("dict elem missing value [key=%v]", key))
|
||||||
|
}
|
||||||
|
|
||||||
|
lastKey = key
|
||||||
|
lastKeyOk = true
|
||||||
|
dict[key] = valuei
|
||||||
|
}
|
||||||
|
return dict
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) parseListInterface() (list []interface{}) {
|
||||||
|
list = []interface{}{}
|
||||||
|
valuei, ok := d.parseValueInterface()
|
||||||
|
for ok {
|
||||||
|
list = append(list, valuei)
|
||||||
|
valuei, ok = d.parseValueInterface()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) getMaxStrLen() int64 {
|
||||||
|
if d.MaxStrLen == 0 {
|
||||||
|
return DefaultDecodeMaxStrLen
|
||||||
|
}
|
||||||
|
return d.MaxStrLen
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,267 @@
|
||||||
|
package bencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
qt "github.com/frankban/quicktest"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type random_decode_test struct {
|
||||||
|
data string
|
||||||
|
expected interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var random_decode_tests = []random_decode_test{
|
||||||
|
{"i57e", int64(57)},
|
||||||
|
{"i-9223372036854775808e", int64(-9223372036854775808)},
|
||||||
|
{"5:hello", "hello"},
|
||||||
|
{"29:unicode test проверка", "unicode test проверка"},
|
||||||
|
{"d1:ai5e1:b5:helloe", map[string]interface{}{"a": int64(5), "b": "hello"}},
|
||||||
|
{
|
||||||
|
"li5ei10ei15ei20e7:bencodee",
|
||||||
|
[]interface{}{int64(5), int64(10), int64(15), int64(20), "bencode"},
|
||||||
|
},
|
||||||
|
{"ldedee", []interface{}{map[string]interface{}{}, map[string]interface{}{}}},
|
||||||
|
{"le", []interface{}{}},
|
||||||
|
{"i604919719469385652980544193299329427705624352086e", func() *big.Int {
|
||||||
|
ret, _ := big.NewInt(-1).SetString("604919719469385652980544193299329427705624352086", 10)
|
||||||
|
return ret
|
||||||
|
}()},
|
||||||
|
{"d1:rd6:\xd4/\xe2F\x00\x01i42ee1:t3:\x9a\x87\x011:v4:TR%=1:y1:re", map[string]interface{}{
|
||||||
|
"r": map[string]interface{}{"\xd4/\xe2F\x00\x01": int64(42)},
|
||||||
|
"t": "\x9a\x87\x01",
|
||||||
|
"v": "TR%=",
|
||||||
|
"y": "r",
|
||||||
|
}},
|
||||||
|
{"d0:i420ee", map[string]interface{}{"": int64(420)}},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRandomDecode(t *testing.T) {
|
||||||
|
for _, test := range random_decode_tests {
|
||||||
|
var value interface{}
|
||||||
|
err := Unmarshal([]byte(test.data), &value)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err, test.data)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
assert.EqualValues(t, test.expected, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoneE(t *testing.T) {
|
||||||
|
var v int
|
||||||
|
err := Unmarshal([]byte("e"), &v)
|
||||||
|
se := err.(*SyntaxError)
|
||||||
|
require.EqualValues(t, 0, se.Offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecoderConsecutive(t *testing.T) {
|
||||||
|
d := NewDecoder(bytes.NewReader([]byte("i1ei2e")))
|
||||||
|
var i int
|
||||||
|
err := d.Decode(&i)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.EqualValues(t, 1, i)
|
||||||
|
err = d.Decode(&i)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.EqualValues(t, 2, i)
|
||||||
|
err = d.Decode(&i)
|
||||||
|
require.Equal(t, io.EOF, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecoderConsecutiveDicts(t *testing.T) {
|
||||||
|
bb := bytes.NewBufferString("d4:herp4:derped3:wat1:ke17:oh baby a triple!")
|
||||||
|
|
||||||
|
d := NewDecoder(bb)
|
||||||
|
assert.EqualValues(t, "d4:herp4:derped3:wat1:ke17:oh baby a triple!", bb.Bytes())
|
||||||
|
assert.EqualValues(t, 0, d.Offset)
|
||||||
|
|
||||||
|
var m map[string]interface{}
|
||||||
|
|
||||||
|
require.NoError(t, d.Decode(&m))
|
||||||
|
assert.Len(t, m, 1)
|
||||||
|
assert.Equal(t, "derp", m["herp"])
|
||||||
|
assert.Equal(t, "d3:wat1:ke17:oh baby a triple!", bb.String())
|
||||||
|
assert.EqualValues(t, 14, d.Offset)
|
||||||
|
|
||||||
|
require.NoError(t, d.Decode(&m))
|
||||||
|
assert.Equal(t, "k", m["wat"])
|
||||||
|
assert.Equal(t, "17:oh baby a triple!", bb.String())
|
||||||
|
assert.EqualValues(t, 24, d.Offset)
|
||||||
|
|
||||||
|
var s string
|
||||||
|
require.NoError(t, d.Decode(&s))
|
||||||
|
assert.Equal(t, "oh baby a triple!", s)
|
||||||
|
assert.EqualValues(t, 44, d.Offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func check_error(t *testing.T, err error) {
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assert_equal(t *testing.T, x, y interface{}) {
|
||||||
|
if !reflect.DeepEqual(x, y) {
|
||||||
|
t.Errorf("got: %v (%T), expected: %v (%T)\n", x, x, y, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type unmarshalerInt struct {
|
||||||
|
x int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *unmarshalerInt) UnmarshalBencode(data []byte) error {
|
||||||
|
return Unmarshal(data, &me.x)
|
||||||
|
}
|
||||||
|
|
||||||
|
type unmarshalerString struct {
|
||||||
|
x string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *unmarshalerString) UnmarshalBencode(data []byte) error {
|
||||||
|
me.x = string(data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalerBencode(t *testing.T) {
|
||||||
|
var i unmarshalerInt
|
||||||
|
var ss []unmarshalerString
|
||||||
|
check_error(t, Unmarshal([]byte("i71e"), &i))
|
||||||
|
assert_equal(t, i.x, 71)
|
||||||
|
check_error(t, Unmarshal([]byte("l5:hello5:fruit3:waye"), &ss))
|
||||||
|
assert_equal(t, ss[0].x, "5:hello")
|
||||||
|
assert_equal(t, ss[1].x, "5:fruit")
|
||||||
|
assert_equal(t, ss[2].x, "3:way")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIgnoreUnmarshalTypeError(t *testing.T) {
|
||||||
|
s := struct {
|
||||||
|
Ignore int `bencode:",ignore_unmarshal_type_error"`
|
||||||
|
Normal int
|
||||||
|
}{}
|
||||||
|
require.Error(t, Unmarshal([]byte("d6:Normal5:helloe"), &s))
|
||||||
|
assert.NoError(t, Unmarshal([]byte("d6:Ignore5:helloe"), &s))
|
||||||
|
qt.Assert(t, Unmarshal([]byte("d6:Ignorei42ee"), &s), qt.IsNil)
|
||||||
|
assert.EqualValues(t, 42, s.Ignore)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test unmarshalling []byte into something that has the same kind but
|
||||||
|
// different type.
|
||||||
|
func TestDecodeCustomSlice(t *testing.T) {
|
||||||
|
type flag byte
|
||||||
|
var fs3, fs2 []flag
|
||||||
|
// We do a longer slice then a shorter slice to see if the buffers are
|
||||||
|
// shared.
|
||||||
|
d := NewDecoder(bytes.NewBufferString("3:\x01\x10\xff2:\x04\x0f"))
|
||||||
|
require.NoError(t, d.Decode(&fs3))
|
||||||
|
require.NoError(t, d.Decode(&fs2))
|
||||||
|
assert.EqualValues(t, []flag{1, 16, 255}, fs3)
|
||||||
|
assert.EqualValues(t, []flag{4, 15}, fs2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalUnusedBytes(t *testing.T) {
|
||||||
|
var i int
|
||||||
|
require.EqualValues(t, ErrUnusedTrailingBytes{1}, Unmarshal([]byte("i42ee"), &i))
|
||||||
|
assert.EqualValues(t, 42, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalByteArray(t *testing.T) {
|
||||||
|
var ba [2]byte
|
||||||
|
assert.NoError(t, Unmarshal([]byte("2:hi"), &ba))
|
||||||
|
assert.EqualValues(t, "hi", ba[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeDictIntoUnsupported(t *testing.T) {
|
||||||
|
// Any type that a dict shouldn't be unmarshallable into.
|
||||||
|
var i int
|
||||||
|
c := qt.New(t)
|
||||||
|
err := Unmarshal([]byte("d1:a1:be"), &i)
|
||||||
|
t.Log(err)
|
||||||
|
c.Check(err, qt.Not(qt.IsNil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalDictKeyNotString(t *testing.T) {
|
||||||
|
// Any type that a dict shouldn't be unmarshallable into.
|
||||||
|
var i int
|
||||||
|
c := qt.New(t)
|
||||||
|
err := Unmarshal([]byte("di42e3:yese"), &i)
|
||||||
|
t.Log(err)
|
||||||
|
c.Check(err, qt.Not(qt.IsNil))
|
||||||
|
}
|
||||||
|
|
||||||
|
type arbitraryReader struct{}
|
||||||
|
|
||||||
|
func (arbitraryReader) Read(b []byte) (int, error) {
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeHugeString(t *testing.T, strLen int64, header, tail string, v interface{}, maxStrLen MaxStrLen) error {
|
||||||
|
r, w := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
fmt.Fprintf(w, header, strLen)
|
||||||
|
io.CopyN(w, arbitraryReader{}, strLen)
|
||||||
|
w.Write([]byte(tail))
|
||||||
|
w.Close()
|
||||||
|
}()
|
||||||
|
d := NewDecoder(r)
|
||||||
|
d.MaxStrLen = maxStrLen
|
||||||
|
return d.Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that bencode strings in various places obey the Decoder.MaxStrLen field.
|
||||||
|
func TestDecodeMaxStrLen(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := qt.New(t)
|
||||||
|
test := func(header, tail string, v interface{}, maxStrLen MaxStrLen) {
|
||||||
|
strLen := maxStrLen
|
||||||
|
if strLen == 0 {
|
||||||
|
strLen = DefaultDecodeMaxStrLen
|
||||||
|
}
|
||||||
|
c.Assert(decodeHugeString(t, strLen, header, tail, v, maxStrLen), qt.IsNil)
|
||||||
|
c.Assert(decodeHugeString(t, strLen+1, header, tail, v, maxStrLen), qt.IsNotNil)
|
||||||
|
}
|
||||||
|
test("d%d:", "i0ee", new(interface{}), 0)
|
||||||
|
test("%d:", "", new(interface{}), DefaultDecodeMaxStrLen)
|
||||||
|
test("%d:", "", new([]byte), 1)
|
||||||
|
test("d3:420%d:", "e", new(struct {
|
||||||
|
Hi []byte `bencode:"420"`
|
||||||
|
}), 69)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is for the "github.com/anacrolix/torrent/metainfo".Info.Private field.
|
||||||
|
func TestDecodeStringIntoBoolPtr(t *testing.T) {
|
||||||
|
var m struct {
|
||||||
|
Private *bool `bencode:"private,omitempty"`
|
||||||
|
}
|
||||||
|
c := qt.New(t)
|
||||||
|
check := func(msg string, expectNil, expectTrue bool) {
|
||||||
|
m.Private = nil
|
||||||
|
c.Check(Unmarshal([]byte(msg), &m), qt.IsNil, qt.Commentf("%q", msg))
|
||||||
|
if expectNil {
|
||||||
|
c.Check(m.Private, qt.IsNil)
|
||||||
|
} else {
|
||||||
|
if c.Check(m.Private, qt.IsNotNil, qt.Commentf("%q", msg)) {
|
||||||
|
c.Check(*m.Private, qt.Equals, expectTrue, qt.Commentf("%q", msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check("d7:privatei1ee", false, true)
|
||||||
|
check("d7:privatei0ee", false, false)
|
||||||
|
check("d7:privatei42ee", false, true)
|
||||||
|
// This is a weird case. We could not allocate the bool to indicate it was bad (maybe a bad
|
||||||
|
// serializer which isn't uncommon), but that requires reworking the decoder to handle
|
||||||
|
// automatically. I think if we cared enough we'd create a custom Unmarshaler. Also if we were
|
||||||
|
// worried enough about performance I'd completely rewrite this package.
|
||||||
|
check("d7:private0:e", false, false)
|
||||||
|
check("d7:private1:te", false, true)
|
||||||
|
check("d7:private5:falsee", false, false)
|
||||||
|
check("d7:private1:Fe", false, false)
|
||||||
|
check("d7:private11:bunnyfoofooe", false, true)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
package bencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/anacrolix/missinggo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isEmptyValue(v reflect.Value) bool {
|
||||||
|
return missinggo.IsEmptyValue(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Encoder struct {
|
||||||
|
w io.Writer
|
||||||
|
scratch [64]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) Encode(v interface{}) (err error) {
|
||||||
|
if v == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if e := recover(); e != nil {
|
||||||
|
if _, ok := e.(runtime.Error); ok {
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
var ok bool
|
||||||
|
err, ok = e.(error)
|
||||||
|
if !ok {
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
e.reflectValue(reflect.ValueOf(v))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type stringValues []reflect.Value
|
||||||
|
|
||||||
|
func (sv stringValues) Len() int { return len(sv) }
|
||||||
|
func (sv stringValues) Swap(i, j int) { sv[i], sv[j] = sv[j], sv[i] }
|
||||||
|
func (sv stringValues) Less(i, j int) bool { return sv.get(i) < sv.get(j) }
|
||||||
|
func (sv stringValues) get(i int) string { return sv[i].String() }
|
||||||
|
|
||||||
|
func (e *Encoder) write(s []byte) {
|
||||||
|
_, err := e.w.Write(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) writeString(s string) {
|
||||||
|
for s != "" {
|
||||||
|
n := copy(e.scratch[:], s)
|
||||||
|
s = s[n:]
|
||||||
|
e.write(e.scratch[:n])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) reflectString(s string) {
|
||||||
|
e.writeStringPrefix(int64(len(s)))
|
||||||
|
e.writeString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) writeStringPrefix(l int64) {
|
||||||
|
b := strconv.AppendInt(e.scratch[:0], l, 10)
|
||||||
|
e.write(b)
|
||||||
|
e.writeString(":")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) reflectByteSlice(s []byte) {
|
||||||
|
e.writeStringPrefix(int64(len(s)))
|
||||||
|
e.write(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the value implements Marshaler interface and marshaling was
|
||||||
|
// done successfully.
|
||||||
|
func (e *Encoder) reflectMarshaler(v reflect.Value) bool {
|
||||||
|
if !v.Type().Implements(marshalerType) {
|
||||||
|
if v.Kind() != reflect.Ptr && v.CanAddr() && v.Addr().Type().Implements(marshalerType) {
|
||||||
|
v = v.Addr()
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m := v.Interface().(Marshaler)
|
||||||
|
data, err := m.MarshalBencode()
|
||||||
|
if err != nil {
|
||||||
|
panic(&MarshalerError{v.Type(), err})
|
||||||
|
}
|
||||||
|
e.write(data)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var bigIntType = reflect.TypeOf((*big.Int)(nil)).Elem()
|
||||||
|
|
||||||
|
func (e *Encoder) reflectValue(v reflect.Value) {
|
||||||
|
if e.reflectMarshaler(v) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Type() == bigIntType {
|
||||||
|
e.writeString("i")
|
||||||
|
bi := v.Interface().(big.Int)
|
||||||
|
e.writeString(bi.String())
|
||||||
|
e.writeString("e")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
if v.Bool() {
|
||||||
|
e.writeString("i1e")
|
||||||
|
} else {
|
||||||
|
e.writeString("i0e")
|
||||||
|
}
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
e.writeString("i")
|
||||||
|
b := strconv.AppendInt(e.scratch[:0], v.Int(), 10)
|
||||||
|
e.write(b)
|
||||||
|
e.writeString("e")
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
e.writeString("i")
|
||||||
|
b := strconv.AppendUint(e.scratch[:0], v.Uint(), 10)
|
||||||
|
e.write(b)
|
||||||
|
e.writeString("e")
|
||||||
|
case reflect.String:
|
||||||
|
e.reflectString(v.String())
|
||||||
|
case reflect.Struct:
|
||||||
|
e.writeString("d")
|
||||||
|
for _, ef := range getEncodeFields(v.Type()) {
|
||||||
|
fieldValue := ef.i(v)
|
||||||
|
if !fieldValue.IsValid() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ef.omitEmpty && isEmptyValue(fieldValue) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.reflectString(ef.tag)
|
||||||
|
e.reflectValue(fieldValue)
|
||||||
|
}
|
||||||
|
e.writeString("e")
|
||||||
|
case reflect.Map:
|
||||||
|
if v.Type().Key().Kind() != reflect.String {
|
||||||
|
panic(&MarshalTypeError{v.Type()})
|
||||||
|
}
|
||||||
|
if v.IsNil() {
|
||||||
|
e.writeString("de")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
e.writeString("d")
|
||||||
|
sv := stringValues(v.MapKeys())
|
||||||
|
sort.Sort(sv)
|
||||||
|
for _, key := range sv {
|
||||||
|
e.reflectString(key.String())
|
||||||
|
e.reflectValue(v.MapIndex(key))
|
||||||
|
}
|
||||||
|
e.writeString("e")
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
e.reflectSequence(v)
|
||||||
|
case reflect.Interface:
|
||||||
|
e.reflectValue(v.Elem())
|
||||||
|
case reflect.Ptr:
|
||||||
|
if v.IsNil() {
|
||||||
|
v = reflect.Zero(v.Type().Elem())
|
||||||
|
} else {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
e.reflectValue(v)
|
||||||
|
default:
|
||||||
|
panic(&MarshalTypeError{v.Type()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) reflectSequence(v reflect.Value) {
|
||||||
|
// Use bencode string-type
|
||||||
|
if v.Type().Elem().Kind() == reflect.Uint8 {
|
||||||
|
if v.Kind() != reflect.Slice {
|
||||||
|
// Can't use []byte optimization
|
||||||
|
if !v.CanAddr() {
|
||||||
|
e.writeStringPrefix(int64(v.Len()))
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
var b [1]byte
|
||||||
|
b[0] = byte(v.Index(i).Uint())
|
||||||
|
e.write(b[:])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v = v.Slice(0, v.Len())
|
||||||
|
}
|
||||||
|
s := v.Bytes()
|
||||||
|
e.reflectByteSlice(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v.IsNil() {
|
||||||
|
e.writeString("le")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.writeString("l")
|
||||||
|
for i, n := 0, v.Len(); i < n; i++ {
|
||||||
|
e.reflectValue(v.Index(i))
|
||||||
|
}
|
||||||
|
e.writeString("e")
|
||||||
|
}
|
||||||
|
|
||||||
|
type encodeField struct {
|
||||||
|
i func(v reflect.Value) reflect.Value
|
||||||
|
tag string
|
||||||
|
omitEmpty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type encodeFieldsSortType []encodeField
|
||||||
|
|
||||||
|
func (ef encodeFieldsSortType) Len() int { return len(ef) }
|
||||||
|
func (ef encodeFieldsSortType) Swap(i, j int) { ef[i], ef[j] = ef[j], ef[i] }
|
||||||
|
func (ef encodeFieldsSortType) Less(i, j int) bool { return ef[i].tag < ef[j].tag }
|
||||||
|
|
||||||
|
var (
|
||||||
|
typeCacheLock sync.RWMutex
|
||||||
|
encodeFieldsCache = make(map[reflect.Type][]encodeField)
|
||||||
|
)
|
||||||
|
|
||||||
|
func getEncodeFields(t reflect.Type) []encodeField {
|
||||||
|
typeCacheLock.RLock()
|
||||||
|
fs, ok := encodeFieldsCache[t]
|
||||||
|
typeCacheLock.RUnlock()
|
||||||
|
if ok {
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
fs = makeEncodeFields(t)
|
||||||
|
typeCacheLock.Lock()
|
||||||
|
defer typeCacheLock.Unlock()
|
||||||
|
encodeFieldsCache[t] = fs
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeEncodeFields(t reflect.Type) (fs []encodeField) {
|
||||||
|
for _i, n := 0, t.NumField(); _i < n; _i++ {
|
||||||
|
i := _i
|
||||||
|
f := t.Field(i)
|
||||||
|
if f.PkgPath != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if f.Anonymous {
|
||||||
|
t := f.Type
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
anonEFs := makeEncodeFields(t)
|
||||||
|
for aefi := range anonEFs {
|
||||||
|
anonEF := anonEFs[aefi]
|
||||||
|
bottomField := anonEF
|
||||||
|
bottomField.i = func(v reflect.Value) reflect.Value {
|
||||||
|
v = v.Field(i)
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
if v.IsNil() {
|
||||||
|
// This will skip serializing this value.
|
||||||
|
return reflect.Value{}
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
return anonEF.i(v)
|
||||||
|
}
|
||||||
|
fs = append(fs, bottomField)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var ef encodeField
|
||||||
|
ef.i = func(v reflect.Value) reflect.Value {
|
||||||
|
return v.Field(i)
|
||||||
|
}
|
||||||
|
ef.tag = f.Name
|
||||||
|
|
||||||
|
tv := getTag(f.Tag)
|
||||||
|
if tv.Ignore() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if tv.Key() != "" {
|
||||||
|
ef.tag = tv.Key()
|
||||||
|
}
|
||||||
|
ef.omitEmpty = tv.OmitEmpty()
|
||||||
|
fs = append(fs, ef)
|
||||||
|
}
|
||||||
|
fss := encodeFieldsSortType(fs)
|
||||||
|
sort.Sort(fss)
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package bencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type random_encode_test struct {
|
||||||
|
value interface{}
|
||||||
|
expected string
|
||||||
|
}
|
||||||
|
|
||||||
|
type random_struct struct {
|
||||||
|
ABC int `bencode:"abc"`
|
||||||
|
SkipThisOne string `bencode:"-"`
|
||||||
|
CDE string
|
||||||
|
}
|
||||||
|
|
||||||
|
type dummy struct {
|
||||||
|
a, b, c int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummy) MarshalBencode() ([]byte, error) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
_, err := fmt.Fprintf(&b, "i%dei%dei%de", d.a+1, d.b+1, d.c+1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var random_encode_tests = []random_encode_test{
|
||||||
|
{int(10), "i10e"},
|
||||||
|
{uint(10), "i10e"},
|
||||||
|
{"hello, world", "12:hello, world"},
|
||||||
|
{true, "i1e"},
|
||||||
|
{false, "i0e"},
|
||||||
|
{int8(-8), "i-8e"},
|
||||||
|
{int16(-16), "i-16e"},
|
||||||
|
{int32(32), "i32e"},
|
||||||
|
{int64(-64), "i-64e"},
|
||||||
|
{uint8(8), "i8e"},
|
||||||
|
{uint16(16), "i16e"},
|
||||||
|
{uint32(32), "i32e"},
|
||||||
|
{uint64(64), "i64e"},
|
||||||
|
{random_struct{123, "nono", "hello"}, "d3:CDE5:hello3:abci123ee"},
|
||||||
|
{map[string]string{"a": "b", "c": "d"}, "d1:a1:b1:c1:de"},
|
||||||
|
{[]byte{1, 2, 3, 4}, "4:\x01\x02\x03\x04"},
|
||||||
|
{&[4]byte{1, 2, 3, 4}, "4:\x01\x02\x03\x04"},
|
||||||
|
{nil, ""},
|
||||||
|
{[]byte{}, "0:"},
|
||||||
|
{[]byte(nil), "0:"},
|
||||||
|
{"", "0:"},
|
||||||
|
{[]int{}, "le"},
|
||||||
|
{map[string]int{}, "de"},
|
||||||
|
{&dummy{1, 2, 3}, "i2ei3ei4e"},
|
||||||
|
{struct {
|
||||||
|
A *string
|
||||||
|
}{nil}, "d1:A0:e"},
|
||||||
|
{struct {
|
||||||
|
A *string
|
||||||
|
}{new(string)}, "d1:A0:e"},
|
||||||
|
{struct {
|
||||||
|
A *string `bencode:",omitempty"`
|
||||||
|
}{nil}, "de"},
|
||||||
|
{struct {
|
||||||
|
A *string `bencode:",omitempty"`
|
||||||
|
}{new(string)}, "d1:A0:e"},
|
||||||
|
{bigIntFromString("62208002200000000000"), "i62208002200000000000e"},
|
||||||
|
{*bigIntFromString("62208002200000000000"), "i62208002200000000000e"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func bigIntFromString(s string) *big.Int {
|
||||||
|
bi, ok := new(big.Int).SetString(s, 10)
|
||||||
|
if !ok {
|
||||||
|
panic(s)
|
||||||
|
}
|
||||||
|
return bi
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRandomEncode(t *testing.T) {
|
||||||
|
for _, test := range random_encode_tests {
|
||||||
|
data, err := Marshal(test.value)
|
||||||
|
assert.NoError(t, err, "%s", test)
|
||||||
|
assert.EqualValues(t, test.expected, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
//go:build go1.18
|
||||||
|
// +build go1.18
|
||||||
|
|
||||||
|
package bencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
qt "github.com/frankban/quicktest"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var bencodeInterfaceChecker = qt.CmpEquals(cmp.Comparer(func(a, b *big.Int) bool {
|
||||||
|
return a.Cmp(b) == 0
|
||||||
|
}))
|
||||||
|
|
||||||
|
func Fuzz(f *testing.F) {
|
||||||
|
for _, ret := range random_encode_tests {
|
||||||
|
f.Add([]byte(ret.expected))
|
||||||
|
}
|
||||||
|
f.Fuzz(func(t *testing.T, b []byte) {
|
||||||
|
c := qt.New(t)
|
||||||
|
var d interface{}
|
||||||
|
err := Unmarshal(b, &d)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
b0, err := Marshal(d)
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
var d0 interface{}
|
||||||
|
err = Unmarshal(b0, &d0)
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
c.Assert(d0, bencodeInterfaceChecker, d)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzInterfaceRoundTrip(f *testing.F) {
|
||||||
|
for _, ret := range random_encode_tests {
|
||||||
|
f.Add([]byte(ret.expected))
|
||||||
|
}
|
||||||
|
f.Fuzz(func(t *testing.T, b []byte) {
|
||||||
|
c := qt.New(t)
|
||||||
|
var d interface{}
|
||||||
|
err := Unmarshal(b, &d)
|
||||||
|
if err != nil {
|
||||||
|
c.Skip(err)
|
||||||
|
}
|
||||||
|
b0, err := Marshal(d)
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
c.Check(b0, qt.DeepEquals, b)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package bencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wow Go is retarded.
|
||||||
|
var (
|
||||||
|
marshalerType = reflect.TypeOf((*Marshaler)(nil)).Elem()
|
||||||
|
unmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package bencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Implements io.ByteScanner over io.Reader, for use in Decoder, to ensure
|
||||||
|
// that as little as the undecoded input Reader is consumed as possible.
|
||||||
|
type scanner struct {
|
||||||
|
r io.Reader
|
||||||
|
b [1]byte // Buffer for ReadByte
|
||||||
|
unread bool // True if b has been unread, and so should be returned next
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *scanner) Read(b []byte) (int, error) {
|
||||||
|
return me.r.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *scanner) ReadByte() (byte, error) {
|
||||||
|
if me.unread {
|
||||||
|
me.unread = false
|
||||||
|
return me.b[0], nil
|
||||||
|
}
|
||||||
|
n, err := me.r.Read(me.b[:])
|
||||||
|
if n == 1 {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return me.b[0], err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *scanner) UnreadByte() error {
|
||||||
|
if me.unread {
|
||||||
|
return errors.New("byte already unread")
|
||||||
|
}
|
||||||
|
me.unread = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
//go:build !go1.20
|
||||||
|
|
||||||
|
package bencode
|
||||||
|
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
func bytesAsString(b []byte) string {
|
||||||
|
return *(*string)(unsafe.Pointer(&b))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
//go:build go1.20
|
||||||
|
|
||||||
|
package bencode
|
||||||
|
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
func bytesAsString(b []byte) string {
|
||||||
|
return unsafe.String(unsafe.SliceData(b), len(b))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package bencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getTag(st reflect.StructTag) tag {
|
||||||
|
return parseTag(st.Get("bencode"))
|
||||||
|
}
|
||||||
|
|
||||||
|
type tag []string
|
||||||
|
|
||||||
|
func parseTag(tagStr string) tag {
|
||||||
|
return strings.Split(tagStr, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me tag) Ignore() bool {
|
||||||
|
return me[0] == "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me tag) Key() string {
|
||||||
|
return me[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me tag) HasOpt(opt string) bool {
|
||||||
|
if len(me) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, s := range me[1:] {
|
||||||
|
if s == opt {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me tag) OmitEmpty() bool {
|
||||||
|
return me.HasOpt("omitempty")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me tag) IgnoreUnmarshalTypeError() bool {
|
||||||
|
return me.HasOpt("ignore_unmarshal_type_error")
|
||||||
|
}
|
||||||
BIN
deps/github.com/anacrolix/torrent/bencode/testdata/archlinux-2011.08.19-netinstall-i686.iso.torrent
vendored
Normal file
BIN
deps/github.com/anacrolix/torrent/bencode/testdata/archlinux-2011.08.19-netinstall-i686.iso.torrent
vendored
Normal file
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,2 @@
|
||||||
|
go test fuzz v1
|
||||||
|
[]byte("i62208002200000000:00{݃y\u007f m.\x16\t\fZL\x18'\xad\xe7\xc4e")
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
go test fuzz v1
|
||||||
|
[]byte("0000000000000000000060000000000000D000000000:0000000000000000000000000000000000000000000000000000000000000000000000000000000000\xa1\xcc!\xc0\x04\a^.000")
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
go test fuzz v1
|
||||||
|
[]byte("00:")
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
go test fuzz v1
|
||||||
|
[]byte("d3:000e")
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
go test fuzz v1
|
||||||
|
[]byte("i+0e")
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
go test fuzz v1
|
||||||
|
[]byte("i00e")
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
go test fuzz v1
|
||||||
|
[]byte("i-0e")
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
go test fuzz v1
|
||||||
|
[]byte("d3:A005:000003:000i0ee")
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
go test fuzz v1
|
||||||
|
[]byte("1:")
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
go test fuzz v1
|
||||||
|
[]byte("d10000000000")
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash/crc32"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
var table = crc32.MakeTable(crc32.Castagnoli)
|
||||||
|
|
||||||
|
type peerPriority = uint32
|
||||||
|
|
||||||
|
func sameSubnet(ones, bits int, a, b net.IP) bool {
|
||||||
|
mask := net.CIDRMask(ones, bits)
|
||||||
|
return a.Mask(mask).Equal(b.Mask(mask))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipv4Mask(a, b net.IP) net.IPMask {
|
||||||
|
if !sameSubnet(16, 32, a, b) {
|
||||||
|
return net.IPv4Mask(0xff, 0xff, 0x55, 0x55)
|
||||||
|
}
|
||||||
|
if !sameSubnet(24, 32, a, b) {
|
||||||
|
return net.IPv4Mask(0xff, 0xff, 0xff, 0x55)
|
||||||
|
}
|
||||||
|
return net.IPv4Mask(0xff, 0xff, 0xff, 0xff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mask(prefix, bytes int) net.IPMask {
|
||||||
|
ret := make(net.IPMask, bytes)
|
||||||
|
for i := range ret {
|
||||||
|
ret[i] = 0x55
|
||||||
|
}
|
||||||
|
for i := 0; i < prefix; i++ {
|
||||||
|
ret[i] = 0xff
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipv6Mask(a, b net.IP) net.IPMask {
|
||||||
|
for i := 6; i <= 16; i++ {
|
||||||
|
if !sameSubnet(i*8, 128, a, b) {
|
||||||
|
return mask(i, 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic(fmt.Sprintf("%s %s", a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func bep40PriorityBytes(a, b IpPort) ([]byte, error) {
|
||||||
|
if a.IP.Equal(b.IP) {
|
||||||
|
var ret [4]byte
|
||||||
|
binary.BigEndian.PutUint16(ret[0:2], a.Port)
|
||||||
|
binary.BigEndian.PutUint16(ret[2:4], b.Port)
|
||||||
|
return ret[:], nil
|
||||||
|
}
|
||||||
|
if a4, b4 := a.IP.To4(), b.IP.To4(); a4 != nil && b4 != nil {
|
||||||
|
m := ipv4Mask(a.IP, b.IP)
|
||||||
|
return append(a4.Mask(m), b4.Mask(m)...), nil
|
||||||
|
}
|
||||||
|
if a6, b6 := a.IP.To16(), b.IP.To16(); a6 != nil && b6 != nil {
|
||||||
|
m := ipv6Mask(a.IP, b.IP)
|
||||||
|
return append(a6.Mask(m), b6.Mask(m)...), nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("incomparable IPs")
|
||||||
|
}
|
||||||
|
|
||||||
|
func bep40Priority(a, b IpPort) (peerPriority, error) {
|
||||||
|
bs, err := bep40PriorityBytes(a, b)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
i := len(bs) / 2
|
||||||
|
_a, _b := bs[:i], bs[i:]
|
||||||
|
if bytes.Compare(_a, _b) > 0 {
|
||||||
|
bs = append(_b, _a...)
|
||||||
|
}
|
||||||
|
return crc32.Checksum(bs, table), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bep40PriorityIgnoreError(a, b IpPort) peerPriority {
|
||||||
|
prio, _ := bep40Priority(a, b)
|
||||||
|
return prio
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBep40Priority(t *testing.T) {
|
||||||
|
assert.EqualValues(t, peerPriority(0xec2d7224), bep40PriorityIgnoreError(
|
||||||
|
IpPort{IP: net.ParseIP("123.213.32.10"), Port: 0},
|
||||||
|
IpPort{IP: net.ParseIP("98.76.54.32"), Port: 0},
|
||||||
|
))
|
||||||
|
assert.EqualValues(t, peerPriority(0xec2d7224), bep40PriorityIgnoreError(
|
||||||
|
IpPort{IP: net.ParseIP("98.76.54.32"), Port: 0},
|
||||||
|
IpPort{IP: net.ParseIP("123.213.32.10"), Port: 0},
|
||||||
|
))
|
||||||
|
assert.Equal(t, peerPriority(0x99568189), bep40PriorityIgnoreError(
|
||||||
|
IpPort{IP: net.ParseIP("123.213.32.10"), Port: 0},
|
||||||
|
IpPort{IP: net.ParseIP("123.213.32.234"), Port: 0},
|
||||||
|
))
|
||||||
|
assert.Equal(t, peerPriority(0x2b41d456), bep40PriorityIgnoreError(
|
||||||
|
IpPort{IP: net.ParseIP("206.248.98.111"), Port: 0},
|
||||||
|
IpPort{IP: net.ParseIP("142.147.89.224"), Port: 0},
|
||||||
|
))
|
||||||
|
assert.EqualValues(t, "\x00\x00\x00\x00", func() []byte {
|
||||||
|
b, _ := bep40PriorityBytes(
|
||||||
|
IpPort{IP: net.ParseIP("123.213.32.234"), Port: 0},
|
||||||
|
IpPort{IP: net.ParseIP("123.213.32.234"), Port: 0},
|
||||||
|
)
|
||||||
|
return b
|
||||||
|
}())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anacrolix/torrent/mse"
|
||||||
|
pp "github.com/anacrolix/torrent/peer_protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are called synchronously, and do not pass ownership of arguments (do not expect to retain
|
||||||
|
// data after returning from the callback). The Client and other locks may still be held. nil
|
||||||
|
// functions are not called.
|
||||||
|
type Callbacks struct {
|
||||||
|
// Called after a peer connection completes the BitTorrent handshake. The Client lock is not
|
||||||
|
// held.
|
||||||
|
CompletedHandshake func(*PeerConn, InfoHash)
|
||||||
|
ReadMessage func(*PeerConn, *pp.Message)
|
||||||
|
ReadExtendedHandshake func(*PeerConn, *pp.ExtendedHandshakeMessage)
|
||||||
|
PeerConnClosed func(*PeerConn)
|
||||||
|
|
||||||
|
// Provides secret keys to be tried against incoming encrypted connections.
|
||||||
|
ReceiveEncryptedHandshakeSkeys mse.SecretKeyIter
|
||||||
|
|
||||||
|
ReceivedUsefulData []func(ReceivedUsefulDataEvent)
|
||||||
|
ReceivedRequested []func(PeerMessageEvent)
|
||||||
|
DeletedRequest []func(PeerRequestEvent)
|
||||||
|
SentRequest []func(PeerRequestEvent)
|
||||||
|
PeerClosed []func(*Peer)
|
||||||
|
NewPeer []func(*Peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReceivedUsefulDataEvent = PeerMessageEvent
|
||||||
|
|
||||||
|
type PeerMessageEvent struct {
|
||||||
|
Peer *Peer
|
||||||
|
Message *pp.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerRequestEvent struct {
|
||||||
|
Peer *Peer
|
||||||
|
Request
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
//go:build !wasm
|
||||||
|
// +build !wasm
|
||||||
|
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
qt "github.com/frankban/quicktest"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/internal/testutil"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBoltPieceCompletionClosedWhenClientClosed(t *testing.T) {
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
pc, err := storage.NewBoltPieceCompletion(cfg.DataDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
ci := storage.NewFileWithCompletion(cfg.DataDir, pc)
|
||||||
|
defer ci.Close()
|
||||||
|
cfg.DefaultStorage = ci
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
cl.Close()
|
||||||
|
// And again, https://github.com/anacrolix/torrent/issues/158
|
||||||
|
cl, err = NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
cl.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssue335(t *testing.T) {
|
||||||
|
dir, mi := testutil.GreetingTestTorrent()
|
||||||
|
defer func() {
|
||||||
|
err := os.RemoveAll(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("removing torrent dummy data dir: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
logErr := func(f func() error, msg string) {
|
||||||
|
err := f()
|
||||||
|
t.Logf("%s: %v", msg, err)
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.Seed = false
|
||||||
|
cfg.Debug = true
|
||||||
|
cfg.DataDir = dir
|
||||||
|
comp, err := storage.NewBoltPieceCompletion(dir)
|
||||||
|
c := qt.New(t)
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
defer logErr(comp.Close, "closing bolt piece completion")
|
||||||
|
mmapStorage := storage.NewMMapWithCompletion(dir, comp)
|
||||||
|
defer logErr(mmapStorage.Close, "closing mmap storage")
|
||||||
|
cfg.DefaultStorage = mmapStorage
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
defer cl.Close()
|
||||||
|
tor, new, err := cl.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
c.Assert(new, qt.IsTrue)
|
||||||
|
c.Assert(cl.WaitAll(), qt.IsTrue)
|
||||||
|
tor.Drop()
|
||||||
|
_, new, err = cl.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
c.Assert(new, qt.IsTrue)
|
||||||
|
c.Assert(cl.WaitAll(), qt.IsTrue)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
g "github.com/anacrolix/generics"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setAdd[K comparable](m *map[K]struct{}, elem K) {
|
||||||
|
g.MakeMapIfNilAndSet(m, elem, struct{}{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientHolepunchAddrSets struct {
|
||||||
|
undialableWithoutHolepunch map[netip.AddrPort]struct{}
|
||||||
|
undialableWithoutHolepunchDialedAfterHolepunchConnect map[netip.AddrPort]struct{}
|
||||||
|
dialableOnlyAfterHolepunch map[netip.AddrPort]struct{}
|
||||||
|
dialedSuccessfullyAfterHolepunchConnect map[netip.AddrPort]struct{}
|
||||||
|
probablyOnlyConnectedDueToHolepunch map[netip.AddrPort]struct{}
|
||||||
|
accepted map[netip.AddrPort]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientStats struct {
|
||||||
|
ConnStats
|
||||||
|
|
||||||
|
// Ongoing outgoing dial attempts. There may be more than one dial going on per peer address due
|
||||||
|
// to hole-punch connect requests. The total may not match the sum of attempts for all Torrents
|
||||||
|
// if a Torrent is dropped while there are outstanding dials.
|
||||||
|
ActiveHalfOpenAttempts int
|
||||||
|
|
||||||
|
NumPeersUndialableWithoutHolepunch int
|
||||||
|
// Number of unique peer addresses that were dialed after receiving a holepunch connect message,
|
||||||
|
// that have previously been undialable without any hole-punching attempts.
|
||||||
|
NumPeersUndialableWithoutHolepunchDialedAfterHolepunchConnect int
|
||||||
|
// Number of unique peer addresses that were successfully dialed and connected after a holepunch
|
||||||
|
// connect message and previously failing to connect without holepunching.
|
||||||
|
NumPeersDialableOnlyAfterHolepunch int
|
||||||
|
NumPeersDialedSuccessfullyAfterHolepunchConnect int
|
||||||
|
NumPeersProbablyOnlyConnectedDueToHolepunch int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *Client) statsLocked() (stats ClientStats) {
|
||||||
|
stats.ConnStats = cl.connStats.Copy()
|
||||||
|
stats.ActiveHalfOpenAttempts = cl.numHalfOpen
|
||||||
|
|
||||||
|
stats.NumPeersUndialableWithoutHolepunch = len(cl.undialableWithoutHolepunch)
|
||||||
|
stats.NumPeersUndialableWithoutHolepunchDialedAfterHolepunchConnect = len(cl.undialableWithoutHolepunchDialedAfterHolepunchConnect)
|
||||||
|
stats.NumPeersDialableOnlyAfterHolepunch = len(cl.dialableOnlyAfterHolepunch)
|
||||||
|
stats.NumPeersDialedSuccessfullyAfterHolepunchConnect = len(cl.dialedSuccessfullyAfterHolepunchConnect)
|
||||||
|
stats.NumPeersProbablyOnlyConnectedDueToHolepunch = len(cl.probablyOnlyConnectedDueToHolepunch)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,909 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"testing/iotest"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anacrolix/dht/v2"
|
||||||
|
"github.com/anacrolix/log"
|
||||||
|
"github.com/anacrolix/missinggo/v2"
|
||||||
|
"github.com/anacrolix/missinggo/v2/filecache"
|
||||||
|
"github.com/frankban/quicktest"
|
||||||
|
qt "github.com/frankban/quicktest"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/bencode"
|
||||||
|
"github.com/anacrolix/torrent/internal/testutil"
|
||||||
|
"github.com/anacrolix/torrent/iplist"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientDefault(t *testing.T) {
|
||||||
|
cl, err := NewClient(TestingConfig(t))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, cl.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientNilConfig(t *testing.T) {
|
||||||
|
// The default config will put crap in the working directory.
|
||||||
|
origDir, _ := os.Getwd()
|
||||||
|
defer os.Chdir(origDir)
|
||||||
|
os.Chdir(t.TempDir())
|
||||||
|
cl, err := NewClient(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, cl.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddDropTorrent(t *testing.T) {
|
||||||
|
cl, err := NewClient(TestingConfig(t))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
dir, mi := testutil.GreetingTestTorrent()
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
tt, new, err := cl.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, new)
|
||||||
|
tt.SetMaxEstablishedConns(0)
|
||||||
|
tt.SetMaxEstablishedConns(1)
|
||||||
|
tt.Drop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddTorrentNoSupportedTrackerSchemes(t *testing.T) {
|
||||||
|
// TODO?
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddTorrentNoUsableURLs(t *testing.T) {
|
||||||
|
// TODO?
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddPeersToUnknownTorrent(t *testing.T) {
|
||||||
|
// TODO?
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPieceHashSize(t *testing.T) {
|
||||||
|
assert.Equal(t, 20, pieceHash.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTorrentInitialState(t *testing.T) {
|
||||||
|
dir, mi := testutil.GreetingTestTorrent()
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
var cl Client
|
||||||
|
cl.init(TestingConfig(t))
|
||||||
|
cl.initLogger()
|
||||||
|
tor := cl.newTorrent(
|
||||||
|
mi.HashInfoBytes(),
|
||||||
|
storage.NewFileWithCompletion(t.TempDir(), storage.NewMapPieceCompletion()),
|
||||||
|
)
|
||||||
|
tor.setChunkSize(2)
|
||||||
|
tor.cl.lock()
|
||||||
|
err := tor.setInfoBytesLocked(mi.InfoBytes)
|
||||||
|
tor.cl.unlock()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, tor.pieces, 3)
|
||||||
|
tor.pendAllChunkSpecs(0)
|
||||||
|
tor.cl.lock()
|
||||||
|
assert.EqualValues(t, 3, tor.pieceNumPendingChunks(0))
|
||||||
|
tor.cl.unlock()
|
||||||
|
assert.EqualValues(t, ChunkSpec{4, 1}, chunkIndexSpec(2, tor.pieceLength(0), tor.chunkSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReducedDialTimeout(t *testing.T) {
|
||||||
|
cfg := NewDefaultClientConfig()
|
||||||
|
for _, _case := range []struct {
|
||||||
|
Max time.Duration
|
||||||
|
HalfOpenLimit int
|
||||||
|
PendingPeers int
|
||||||
|
ExpectedReduced time.Duration
|
||||||
|
}{
|
||||||
|
{cfg.NominalDialTimeout, 40, 0, cfg.NominalDialTimeout},
|
||||||
|
{cfg.NominalDialTimeout, 40, 1, cfg.NominalDialTimeout},
|
||||||
|
{cfg.NominalDialTimeout, 40, 39, cfg.NominalDialTimeout},
|
||||||
|
{cfg.NominalDialTimeout, 40, 40, cfg.NominalDialTimeout / 2},
|
||||||
|
{cfg.NominalDialTimeout, 40, 80, cfg.NominalDialTimeout / 3},
|
||||||
|
{cfg.NominalDialTimeout, 40, 4000, cfg.NominalDialTimeout / 101},
|
||||||
|
} {
|
||||||
|
reduced := reducedDialTimeout(cfg.MinDialTimeout, _case.Max, _case.HalfOpenLimit, _case.PendingPeers)
|
||||||
|
expected := _case.ExpectedReduced
|
||||||
|
if expected < cfg.MinDialTimeout {
|
||||||
|
expected = cfg.MinDialTimeout
|
||||||
|
}
|
||||||
|
if reduced != expected {
|
||||||
|
t.Fatalf("expected %s, got %s", _case.ExpectedReduced, reduced)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddDropManyTorrents(t *testing.T) {
|
||||||
|
cl, err := NewClient(TestingConfig(t))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
for i := 0; i < 1000; i += 1 {
|
||||||
|
var spec TorrentSpec
|
||||||
|
binary.PutVarint(spec.InfoHash[:], int64(i))
|
||||||
|
tt, new, err := cl.AddTorrentSpec(&spec)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, new)
|
||||||
|
defer tt.Drop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileCachePieceResourceStorage(fc *filecache.Cache) storage.ClientImpl {
|
||||||
|
return storage.NewResourcePiecesOpts(
|
||||||
|
fc.AsResourceProvider(),
|
||||||
|
storage.ResourcePiecesOpts{
|
||||||
|
LeaveIncompleteChunks: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergingTrackersByAddingSpecs(t *testing.T) {
|
||||||
|
cl, err := NewClient(TestingConfig(t))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
spec := TorrentSpec{}
|
||||||
|
T, new, _ := cl.AddTorrentSpec(&spec)
|
||||||
|
if !new {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
spec.Trackers = [][]string{{"http://a"}, {"udp://b"}}
|
||||||
|
_, new, _ = cl.AddTorrentSpec(&spec)
|
||||||
|
assert.False(t, new)
|
||||||
|
assert.EqualValues(t, [][]string{{"http://a"}, {"udp://b"}}, T.metainfo.AnnounceList)
|
||||||
|
// Because trackers are disabled in TestingConfig.
|
||||||
|
assert.EqualValues(t, 0, len(T.trackerAnnouncers))
|
||||||
|
}
|
||||||
|
|
||||||
|
// We read from a piece which is marked completed, but is missing data.
|
||||||
|
func TestCompletedPieceWrongSize(t *testing.T) {
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.DefaultStorage = badStorage{}
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
info := metainfo.Info{
|
||||||
|
PieceLength: 15,
|
||||||
|
Pieces: make([]byte, 20),
|
||||||
|
Files: []metainfo.FileInfo{
|
||||||
|
{Path: []string{"greeting"}, Length: 13},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b, err := bencode.Marshal(info)
|
||||||
|
require.NoError(t, err)
|
||||||
|
tt, new, err := cl.AddTorrentSpec(&TorrentSpec{
|
||||||
|
InfoBytes: b,
|
||||||
|
InfoHash: metainfo.HashBytes(b),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer tt.Drop()
|
||||||
|
assert.True(t, new)
|
||||||
|
r := tt.NewReader()
|
||||||
|
defer r.Close()
|
||||||
|
quicktest.Check(t, iotest.TestReader(r, []byte(testutil.GreetingFileContents)), quicktest.IsNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAddLargeTorrent(b *testing.B) {
|
||||||
|
cfg := TestingConfig(b)
|
||||||
|
cfg.DisableTCP = true
|
||||||
|
cfg.DisableUTP = true
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
require.NoError(b, err)
|
||||||
|
defer cl.Close()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i += 1 {
|
||||||
|
t, err := cl.AddTorrentFromFile("testdata/bootstrap.dat.torrent")
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Drop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResponsive(t *testing.T) {
|
||||||
|
seederDataDir, mi := testutil.GreetingTestTorrent()
|
||||||
|
defer os.RemoveAll(seederDataDir)
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.Seed = true
|
||||||
|
cfg.DataDir = seederDataDir
|
||||||
|
seeder, err := NewClient(cfg)
|
||||||
|
require.Nil(t, err)
|
||||||
|
defer seeder.Close()
|
||||||
|
seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
|
||||||
|
seederTorrent.VerifyData()
|
||||||
|
leecherDataDir := t.TempDir()
|
||||||
|
cfg = TestingConfig(t)
|
||||||
|
cfg.DataDir = leecherDataDir
|
||||||
|
leecher, err := NewClient(cfg)
|
||||||
|
require.Nil(t, err)
|
||||||
|
defer leecher.Close()
|
||||||
|
leecherTorrent, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
|
||||||
|
ret = TorrentSpecFromMetaInfo(mi)
|
||||||
|
ret.ChunkSize = 2
|
||||||
|
return
|
||||||
|
}())
|
||||||
|
leecherTorrent.AddClientPeer(seeder)
|
||||||
|
reader := leecherTorrent.NewReader()
|
||||||
|
defer reader.Close()
|
||||||
|
reader.SetReadahead(0)
|
||||||
|
reader.SetResponsive()
|
||||||
|
b := make([]byte, 2)
|
||||||
|
_, err = reader.Seek(3, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = io.ReadFull(reader, b)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.EqualValues(t, "lo", string(b))
|
||||||
|
_, err = reader.Seek(11, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
n, err := io.ReadFull(reader, b)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.EqualValues(t, 2, n)
|
||||||
|
assert.EqualValues(t, "d\n", string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestResponsive was the first test to fail if uTP is disabled and TCP sockets dial from the
|
||||||
|
// listening port.
|
||||||
|
func TestResponsiveTcpOnly(t *testing.T) {
|
||||||
|
seederDataDir, mi := testutil.GreetingTestTorrent()
|
||||||
|
defer os.RemoveAll(seederDataDir)
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.DisableUTP = true
|
||||||
|
cfg.Seed = true
|
||||||
|
cfg.DataDir = seederDataDir
|
||||||
|
seeder, err := NewClient(cfg)
|
||||||
|
require.Nil(t, err)
|
||||||
|
defer seeder.Close()
|
||||||
|
seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
|
||||||
|
seederTorrent.VerifyData()
|
||||||
|
leecherDataDir := t.TempDir()
|
||||||
|
cfg = TestingConfig(t)
|
||||||
|
cfg.DataDir = leecherDataDir
|
||||||
|
leecher, err := NewClient(cfg)
|
||||||
|
require.Nil(t, err)
|
||||||
|
defer leecher.Close()
|
||||||
|
leecherTorrent, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
|
||||||
|
ret = TorrentSpecFromMetaInfo(mi)
|
||||||
|
ret.ChunkSize = 2
|
||||||
|
return
|
||||||
|
}())
|
||||||
|
leecherTorrent.AddClientPeer(seeder)
|
||||||
|
reader := leecherTorrent.NewReader()
|
||||||
|
defer reader.Close()
|
||||||
|
reader.SetReadahead(0)
|
||||||
|
reader.SetResponsive()
|
||||||
|
b := make([]byte, 2)
|
||||||
|
_, err = reader.Seek(3, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = io.ReadFull(reader, b)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.EqualValues(t, "lo", string(b))
|
||||||
|
_, err = reader.Seek(11, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
n, err := io.ReadFull(reader, b)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.EqualValues(t, 2, n)
|
||||||
|
assert.EqualValues(t, "d\n", string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTorrentDroppedDuringResponsiveRead(t *testing.T) {
|
||||||
|
seederDataDir, mi := testutil.GreetingTestTorrent()
|
||||||
|
defer os.RemoveAll(seederDataDir)
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.Seed = true
|
||||||
|
cfg.DataDir = seederDataDir
|
||||||
|
seeder, err := NewClient(cfg)
|
||||||
|
require.Nil(t, err)
|
||||||
|
defer seeder.Close()
|
||||||
|
seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
|
||||||
|
seederTorrent.VerifyData()
|
||||||
|
leecherDataDir := t.TempDir()
|
||||||
|
cfg = TestingConfig(t)
|
||||||
|
cfg.DataDir = leecherDataDir
|
||||||
|
leecher, err := NewClient(cfg)
|
||||||
|
require.Nil(t, err)
|
||||||
|
defer leecher.Close()
|
||||||
|
leecherTorrent, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
|
||||||
|
ret = TorrentSpecFromMetaInfo(mi)
|
||||||
|
ret.ChunkSize = 2
|
||||||
|
return
|
||||||
|
}())
|
||||||
|
leecherTorrent.AddClientPeer(seeder)
|
||||||
|
reader := leecherTorrent.NewReader()
|
||||||
|
defer reader.Close()
|
||||||
|
reader.SetReadahead(0)
|
||||||
|
reader.SetResponsive()
|
||||||
|
b := make([]byte, 2)
|
||||||
|
_, err = reader.Seek(3, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = io.ReadFull(reader, b)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.EqualValues(t, "lo", string(b))
|
||||||
|
_, err = reader.Seek(11, io.SeekStart)
|
||||||
|
require.NoError(t, err)
|
||||||
|
leecherTorrent.Drop()
|
||||||
|
n, err := reader.Read(b)
|
||||||
|
assert.EqualError(t, err, "torrent closed")
|
||||||
|
assert.EqualValues(t, 0, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDhtInheritBlocklist(t *testing.T) {
|
||||||
|
ipl := iplist.New(nil)
|
||||||
|
require.NotNil(t, ipl)
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.IPBlocklist = ipl
|
||||||
|
cfg.NoDHT = false
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
numServers := 0
|
||||||
|
cl.eachDhtServer(func(s DhtServer) {
|
||||||
|
t.Log(s)
|
||||||
|
assert.Equal(t, ipl, s.(AnacrolixDhtServerWrapper).Server.IPBlocklist())
|
||||||
|
numServers++
|
||||||
|
})
|
||||||
|
assert.EqualValues(t, 2, numServers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that stuff is merged in subsequent AddTorrentSpec for the same
|
||||||
|
// infohash.
|
||||||
|
func TestAddTorrentSpecMerging(t *testing.T) {
|
||||||
|
cl, err := NewClient(TestingConfig(t))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
dir, mi := testutil.GreetingTestTorrent()
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
tt, new, err := cl.AddTorrentSpec(&TorrentSpec{
|
||||||
|
InfoHash: mi.HashInfoBytes(),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, new)
|
||||||
|
require.Nil(t, tt.Info())
|
||||||
|
_, new, err = cl.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, new)
|
||||||
|
require.NotNil(t, tt.Info())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTorrentDroppedBeforeGotInfo(t *testing.T) {
|
||||||
|
dir, mi := testutil.GreetingTestTorrent()
|
||||||
|
os.RemoveAll(dir)
|
||||||
|
cl, _ := NewClient(TestingConfig(t))
|
||||||
|
defer cl.Close()
|
||||||
|
tt, _, _ := cl.AddTorrentSpec(&TorrentSpec{
|
||||||
|
InfoHash: mi.HashInfoBytes(),
|
||||||
|
})
|
||||||
|
tt.Drop()
|
||||||
|
assert.EqualValues(t, 0, len(cl.Torrents()))
|
||||||
|
select {
|
||||||
|
case <-tt.GotInfo():
|
||||||
|
t.FailNow()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTorrentData(ts *storage.Torrent, info metainfo.Info, b []byte) {
|
||||||
|
for i := 0; i < info.NumPieces(); i += 1 {
|
||||||
|
p := info.Piece(i)
|
||||||
|
ts.Piece(p).WriteAt(b[p.Offset():p.Offset()+p.Length()], 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAddTorrentPriorPieceCompletion(t *testing.T, alreadyCompleted bool, csf func(*filecache.Cache) storage.ClientImpl) {
|
||||||
|
fileCacheDir := t.TempDir()
|
||||||
|
fileCache, err := filecache.NewCache(fileCacheDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
greetingDataTempDir, greetingMetainfo := testutil.GreetingTestTorrent()
|
||||||
|
defer os.RemoveAll(greetingDataTempDir)
|
||||||
|
filePieceStore := csf(fileCache)
|
||||||
|
info, err := greetingMetainfo.UnmarshalInfo()
|
||||||
|
require.NoError(t, err)
|
||||||
|
ih := greetingMetainfo.HashInfoBytes()
|
||||||
|
greetingData, err := storage.NewClient(filePieceStore).OpenTorrent(&info, ih)
|
||||||
|
require.NoError(t, err)
|
||||||
|
writeTorrentData(greetingData, info, []byte(testutil.GreetingFileContents))
|
||||||
|
// require.Equal(t, len(testutil.GreetingFileContents), written)
|
||||||
|
// require.NoError(t, err)
|
||||||
|
for i := 0; i < info.NumPieces(); i++ {
|
||||||
|
p := info.Piece(i)
|
||||||
|
if alreadyCompleted {
|
||||||
|
require.NoError(t, greetingData.Piece(p).MarkComplete())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
// TODO: Disable network option?
|
||||||
|
cfg.DisableTCP = true
|
||||||
|
cfg.DisableUTP = true
|
||||||
|
cfg.DefaultStorage = filePieceStore
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
tt, err := cl.AddTorrent(greetingMetainfo)
|
||||||
|
require.NoError(t, err)
|
||||||
|
psrs := tt.PieceStateRuns()
|
||||||
|
assert.Len(t, psrs, 1)
|
||||||
|
assert.EqualValues(t, 3, psrs[0].Length)
|
||||||
|
assert.Equal(t, alreadyCompleted, psrs[0].Complete)
|
||||||
|
if alreadyCompleted {
|
||||||
|
r := tt.NewReader()
|
||||||
|
quicktest.Check(t, iotest.TestReader(r, []byte(testutil.GreetingFileContents)), quicktest.IsNil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddTorrentPiecesAlreadyCompleted(t *testing.T) {
|
||||||
|
testAddTorrentPriorPieceCompletion(t, true, fileCachePieceResourceStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddTorrentPiecesNotAlreadyCompleted(t *testing.T) {
|
||||||
|
testAddTorrentPriorPieceCompletion(t, false, fileCachePieceResourceStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddMetainfoWithNodes(t *testing.T) {
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.ListenHost = func(string) string { return "" }
|
||||||
|
cfg.NoDHT = false
|
||||||
|
cfg.DhtStartingNodes = func(string) dht.StartingNodesGetter { return func() ([]dht.Addr, error) { return nil, nil } }
|
||||||
|
// For now, we want to just jam the nodes into the table, without verifying them first. Also the
|
||||||
|
// DHT code doesn't support mixing secure and insecure nodes if security is enabled (yet).
|
||||||
|
// cfg.DHTConfig.NoSecurity = true
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
sum := func() (ret int64) {
|
||||||
|
cl.eachDhtServer(func(s DhtServer) {
|
||||||
|
ret += s.Stats().(dht.ServerStats).OutboundQueriesAttempted
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.EqualValues(t, 0, sum())
|
||||||
|
tt, err := cl.AddTorrentFromFile("metainfo/testdata/issue_65a.torrent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
// Nodes are not added or exposed in Torrent's metainfo. We just randomly
|
||||||
|
// check if the announce-list is here instead. TODO: Add nodes.
|
||||||
|
assert.Len(t, tt.metainfo.AnnounceList, 5)
|
||||||
|
// There are 6 nodes in the torrent file.
|
||||||
|
for sum() != int64(6*len(cl.dhtServers)) {
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testDownloadCancelParams struct {
|
||||||
|
SetLeecherStorageCapacity bool
|
||||||
|
LeecherStorageCapacity int64
|
||||||
|
Cancel bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDownloadCancel(t *testing.T, ps testDownloadCancelParams) {
|
||||||
|
greetingTempDir, mi := testutil.GreetingTestTorrent()
|
||||||
|
defer os.RemoveAll(greetingTempDir)
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.Seed = true
|
||||||
|
cfg.DataDir = greetingTempDir
|
||||||
|
seeder, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer seeder.Close()
|
||||||
|
defer testutil.ExportStatusWriter(seeder, "s", t)()
|
||||||
|
seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
|
||||||
|
seederTorrent.VerifyData()
|
||||||
|
leecherDataDir := t.TempDir()
|
||||||
|
fc, err := filecache.NewCache(leecherDataDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if ps.SetLeecherStorageCapacity {
|
||||||
|
fc.SetCapacity(ps.LeecherStorageCapacity)
|
||||||
|
}
|
||||||
|
cfg.DefaultStorage = storage.NewResourcePieces(fc.AsResourceProvider())
|
||||||
|
cfg.DataDir = leecherDataDir
|
||||||
|
leecher, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer leecher.Close()
|
||||||
|
defer testutil.ExportStatusWriter(leecher, "l", t)()
|
||||||
|
leecherGreeting, new, err := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
|
||||||
|
ret = TorrentSpecFromMetaInfo(mi)
|
||||||
|
ret.ChunkSize = 2
|
||||||
|
return
|
||||||
|
}())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, new)
|
||||||
|
psc := leecherGreeting.SubscribePieceStateChanges()
|
||||||
|
defer psc.Close()
|
||||||
|
|
||||||
|
leecherGreeting.cl.lock()
|
||||||
|
leecherGreeting.downloadPiecesLocked(0, leecherGreeting.numPieces())
|
||||||
|
if ps.Cancel {
|
||||||
|
leecherGreeting.cancelPiecesLocked(0, leecherGreeting.NumPieces(), "")
|
||||||
|
}
|
||||||
|
leecherGreeting.cl.unlock()
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
|
go leecherGreeting.AddClientPeer(seeder)
|
||||||
|
completes := make(map[int]bool, 3)
|
||||||
|
expected := func() map[int]bool {
|
||||||
|
if ps.Cancel {
|
||||||
|
return map[int]bool{0: false, 1: false, 2: false}
|
||||||
|
} else {
|
||||||
|
return map[int]bool{0: true, 1: true, 2: true}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for !reflect.DeepEqual(completes, expected) {
|
||||||
|
v := <-psc.Values
|
||||||
|
completes[v.Index] = v.Complete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTorrentDownloadAll(t *testing.T) {
|
||||||
|
testDownloadCancel(t, testDownloadCancelParams{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTorrentDownloadAllThenCancel(t *testing.T) {
|
||||||
|
testDownloadCancel(t, testDownloadCancelParams{
|
||||||
|
Cancel: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that it's an error for a peer to send an invalid have message.
|
||||||
|
func TestPeerInvalidHave(t *testing.T) {
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.DropMutuallyCompletePeers = false
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
info := metainfo.Info{
|
||||||
|
PieceLength: 1,
|
||||||
|
Pieces: make([]byte, 20),
|
||||||
|
Files: []metainfo.FileInfo{{Length: 1}},
|
||||||
|
}
|
||||||
|
infoBytes, err := bencode.Marshal(info)
|
||||||
|
require.NoError(t, err)
|
||||||
|
tt, _new, err := cl.AddTorrentSpec(&TorrentSpec{
|
||||||
|
InfoBytes: infoBytes,
|
||||||
|
InfoHash: metainfo.HashBytes(infoBytes),
|
||||||
|
Storage: badStorage{},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, _new)
|
||||||
|
defer tt.Drop()
|
||||||
|
cn := &PeerConn{Peer: Peer{
|
||||||
|
t: tt,
|
||||||
|
callbacks: &cfg.Callbacks,
|
||||||
|
}}
|
||||||
|
tt.conns[cn] = struct{}{}
|
||||||
|
cn.peerImpl = cn
|
||||||
|
cl.lock()
|
||||||
|
defer cl.unlock()
|
||||||
|
assert.NoError(t, cn.peerSentHave(0))
|
||||||
|
assert.Error(t, cn.peerSentHave(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPieceCompletedInStorageButNotClient(t *testing.T) {
|
||||||
|
greetingTempDir, greetingMetainfo := testutil.GreetingTestTorrent()
|
||||||
|
defer os.RemoveAll(greetingTempDir)
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.DataDir = greetingTempDir
|
||||||
|
seeder, err := NewClient(TestingConfig(t))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer seeder.Close()
|
||||||
|
seeder.AddTorrentSpec(&TorrentSpec{
|
||||||
|
InfoBytes: greetingMetainfo.InfoBytes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that when the listen port is 0, all the protocols listened on have
|
||||||
|
// the same port, and it isn't zero.
|
||||||
|
func TestClientDynamicListenPortAllProtocols(t *testing.T) {
|
||||||
|
cl, err := NewClient(TestingConfig(t))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
port := cl.LocalPort()
|
||||||
|
assert.NotEqual(t, 0, port)
|
||||||
|
cl.eachListener(func(s Listener) bool {
|
||||||
|
assert.Equal(t, port, missinggo.AddrPort(s.Addr()))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientDynamicListenTCPOnly(t *testing.T) {
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.DisableUTP = true
|
||||||
|
cfg.DisableTCP = false
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
assert.NotEqual(t, 0, cl.LocalPort())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientDynamicListenUTPOnly(t *testing.T) {
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.DisableTCP = true
|
||||||
|
cfg.DisableUTP = false
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
assert.NotEqual(t, 0, cl.LocalPort())
|
||||||
|
}
|
||||||
|
|
||||||
|
func totalConns(tts []*Torrent) (ret int) {
|
||||||
|
for _, tt := range tts {
|
||||||
|
tt.cl.lock()
|
||||||
|
ret += len(tt.conns)
|
||||||
|
tt.cl.unlock()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetMaxEstablishedConn(t *testing.T) {
|
||||||
|
var tts []*Torrent
|
||||||
|
ih := testutil.GreetingMetaInfo().HashInfoBytes()
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.DisableAcceptRateLimiting = true
|
||||||
|
cfg.DropDuplicatePeerIds = true
|
||||||
|
for i := 0; i < 3; i += 1 {
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
tt, _ := cl.AddTorrentInfoHash(ih)
|
||||||
|
tt.SetMaxEstablishedConns(2)
|
||||||
|
defer testutil.ExportStatusWriter(cl, fmt.Sprintf("%d", i), t)()
|
||||||
|
tts = append(tts, tt)
|
||||||
|
}
|
||||||
|
addPeers := func() {
|
||||||
|
for _, tt := range tts {
|
||||||
|
for _, _tt := range tts {
|
||||||
|
// if tt != _tt {
|
||||||
|
tt.AddClientPeer(_tt.cl)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
waitTotalConns := func(num int) {
|
||||||
|
for totalConns(tts) != num {
|
||||||
|
addPeers()
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addPeers()
|
||||||
|
waitTotalConns(6)
|
||||||
|
tts[0].SetMaxEstablishedConns(1)
|
||||||
|
waitTotalConns(4)
|
||||||
|
tts[0].SetMaxEstablishedConns(0)
|
||||||
|
waitTotalConns(2)
|
||||||
|
tts[0].SetMaxEstablishedConns(1)
|
||||||
|
addPeers()
|
||||||
|
waitTotalConns(4)
|
||||||
|
tts[0].SetMaxEstablishedConns(2)
|
||||||
|
addPeers()
|
||||||
|
waitTotalConns(6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a file containing its own name as data. Make a metainfo from that, adds it to the given
|
||||||
|
// client, and returns a magnet link.
|
||||||
|
func makeMagnet(t *testing.T, cl *Client, dir, name string) string {
|
||||||
|
os.MkdirAll(dir, 0o770)
|
||||||
|
file, err := os.Create(filepath.Join(dir, name))
|
||||||
|
require.NoError(t, err)
|
||||||
|
file.Write([]byte(name))
|
||||||
|
file.Close()
|
||||||
|
mi := metainfo.MetaInfo{}
|
||||||
|
mi.SetDefaults()
|
||||||
|
info := metainfo.Info{PieceLength: 256 * 1024}
|
||||||
|
err = info.BuildFromFilePath(filepath.Join(dir, name))
|
||||||
|
require.NoError(t, err)
|
||||||
|
mi.InfoBytes, err = bencode.Marshal(info)
|
||||||
|
require.NoError(t, err)
|
||||||
|
magnet := mi.Magnet(nil, &info).String()
|
||||||
|
tr, err := cl.AddTorrent(&mi)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, tr.Seeding())
|
||||||
|
tr.VerifyData()
|
||||||
|
return magnet
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/anacrolix/torrent/issues/114
|
||||||
|
func TestMultipleTorrentsWithEncryption(t *testing.T) {
|
||||||
|
testSeederLeecherPair(
|
||||||
|
t,
|
||||||
|
func(cfg *ClientConfig) {
|
||||||
|
cfg.HeaderObfuscationPolicy.Preferred = true
|
||||||
|
cfg.HeaderObfuscationPolicy.RequirePreferred = true
|
||||||
|
},
|
||||||
|
func(cfg *ClientConfig) {
|
||||||
|
cfg.HeaderObfuscationPolicy.RequirePreferred = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that the leecher can download a torrent in its entirety from the seeder. Note that the
|
||||||
|
// seeder config is done first.
|
||||||
|
func testSeederLeecherPair(t *testing.T, seeder, leecher func(*ClientConfig)) {
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.Seed = true
|
||||||
|
cfg.DataDir = filepath.Join(cfg.DataDir, "server")
|
||||||
|
os.Mkdir(cfg.DataDir, 0o755)
|
||||||
|
seeder(cfg)
|
||||||
|
server, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer server.Close()
|
||||||
|
defer testutil.ExportStatusWriter(server, "s", t)()
|
||||||
|
magnet1 := makeMagnet(t, server, cfg.DataDir, "test1")
|
||||||
|
// Extra torrents are added to test the seeder having to match incoming obfuscated headers
|
||||||
|
// against more than one torrent. See issue #114
|
||||||
|
makeMagnet(t, server, cfg.DataDir, "test2")
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
makeMagnet(t, server, cfg.DataDir, fmt.Sprintf("test%d", i+2))
|
||||||
|
}
|
||||||
|
cfg = TestingConfig(t)
|
||||||
|
cfg.DataDir = filepath.Join(cfg.DataDir, "client")
|
||||||
|
leecher(cfg)
|
||||||
|
client, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer client.Close()
|
||||||
|
defer testutil.ExportStatusWriter(client, "c", t)()
|
||||||
|
tr, err := client.AddMagnet(magnet1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
tr.AddClientPeer(server)
|
||||||
|
<-tr.GotInfo()
|
||||||
|
tr.DownloadAll()
|
||||||
|
client.WaitAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// This appears to be the situation with the S3 BitTorrent client.
|
||||||
|
func TestObfuscatedHeaderFallbackSeederDisallowsLeecherPrefers(t *testing.T) {
|
||||||
|
// Leecher prefers obfuscation, but the seeder does not allow it.
|
||||||
|
testSeederLeecherPair(
|
||||||
|
t,
|
||||||
|
func(cfg *ClientConfig) {
|
||||||
|
cfg.HeaderObfuscationPolicy.Preferred = false
|
||||||
|
cfg.HeaderObfuscationPolicy.RequirePreferred = true
|
||||||
|
},
|
||||||
|
func(cfg *ClientConfig) {
|
||||||
|
cfg.HeaderObfuscationPolicy.Preferred = true
|
||||||
|
cfg.HeaderObfuscationPolicy.RequirePreferred = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObfuscatedHeaderFallbackSeederRequiresLeecherPrefersNot(t *testing.T) {
|
||||||
|
// Leecher prefers no obfuscation, but the seeder enforces it.
|
||||||
|
testSeederLeecherPair(
|
||||||
|
t,
|
||||||
|
func(cfg *ClientConfig) {
|
||||||
|
cfg.HeaderObfuscationPolicy.Preferred = true
|
||||||
|
cfg.HeaderObfuscationPolicy.RequirePreferred = true
|
||||||
|
},
|
||||||
|
func(cfg *ClientConfig) {
|
||||||
|
cfg.HeaderObfuscationPolicy.Preferred = false
|
||||||
|
cfg.HeaderObfuscationPolicy.RequirePreferred = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientAddressInUse(t *testing.T) {
|
||||||
|
s, _ := NewUtpSocket("udp", "localhost:50007", nil, log.Default)
|
||||||
|
if s != nil {
|
||||||
|
defer s.Close()
|
||||||
|
}
|
||||||
|
cfg := TestingConfig(t).SetListenAddr("localhost:50007")
|
||||||
|
cfg.DisableUTP = false
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
if err == nil {
|
||||||
|
assert.Nil(t, cl.Close())
|
||||||
|
}
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, cl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientHasDhtServersWhenUtpDisabled(t *testing.T) {
|
||||||
|
cc := TestingConfig(t)
|
||||||
|
cc.DisableUTP = true
|
||||||
|
cc.NoDHT = false
|
||||||
|
cl, err := NewClient(cc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
assert.NotEmpty(t, cl.DhtServers())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientDisabledImplicitNetworksButDhtEnabled(t *testing.T) {
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.DisableTCP = true
|
||||||
|
cfg.DisableUTP = true
|
||||||
|
cfg.NoDHT = false
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
assert.Empty(t, cl.listeners)
|
||||||
|
assert.NotEmpty(t, cl.DhtServers())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadPeerIpPort(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
title string
|
||||||
|
ip net.IP
|
||||||
|
port int
|
||||||
|
expectedOk bool
|
||||||
|
setup func(*Client)
|
||||||
|
}{
|
||||||
|
{"empty both", nil, 0, true, func(*Client) {}},
|
||||||
|
{"empty/nil ip", nil, 6666, true, func(*Client) {}},
|
||||||
|
{
|
||||||
|
"empty port",
|
||||||
|
net.ParseIP("127.0.0.1/32"),
|
||||||
|
0, true,
|
||||||
|
func(*Client) {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in doppleganger addresses",
|
||||||
|
net.ParseIP("127.0.0.1/32"),
|
||||||
|
2322,
|
||||||
|
true,
|
||||||
|
func(cl *Client) {
|
||||||
|
cl.dopplegangerAddrs["10.0.0.1:2322"] = struct{}{}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in IP block list",
|
||||||
|
net.ParseIP("10.0.0.1"),
|
||||||
|
2322,
|
||||||
|
true,
|
||||||
|
func(cl *Client) {
|
||||||
|
cl.ipBlockList = iplist.New([]iplist.Range{
|
||||||
|
{First: net.ParseIP("10.0.0.1"), Last: net.ParseIP("10.0.0.255")},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in bad peer IPs",
|
||||||
|
net.ParseIP("10.0.0.1"),
|
||||||
|
2322,
|
||||||
|
true,
|
||||||
|
func(cl *Client) {
|
||||||
|
ipAddr, ok := netip.AddrFromSlice(net.ParseIP("10.0.0.1"))
|
||||||
|
require.True(t, ok)
|
||||||
|
cl.badPeerIPs = map[netip.Addr]struct{}{}
|
||||||
|
cl.badPeerIPs[ipAddr] = struct{}{}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"good",
|
||||||
|
net.ParseIP("10.0.0.1"),
|
||||||
|
2322,
|
||||||
|
false,
|
||||||
|
func(cl *Client) {},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.title, func(t *testing.T) {
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.DisableTCP = true
|
||||||
|
cfg.DisableUTP = true
|
||||||
|
cfg.NoDHT = false
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cl.Close()
|
||||||
|
|
||||||
|
tc.setup(cl)
|
||||||
|
require.Equal(t, tc.expectedOk, cl.badPeerIPPort(tc.ip, tc.port))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/anacrolix/torrent/issues/837
|
||||||
|
func TestClientConfigSetHandlerNotIgnored(t *testing.T) {
|
||||||
|
cfg := TestingConfig(t)
|
||||||
|
cfg.Logger.SetHandlers(log.DiscardHandler)
|
||||||
|
c := qt.New(t)
|
||||||
|
cl, err := NewClient(cfg)
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
defer cl.Close()
|
||||||
|
c.Assert(cl.logger.Handlers, qt.HasLen, 1)
|
||||||
|
h := cl.logger.Handlers[0].(log.StreamHandler)
|
||||||
|
c.Check(h.W, qt.Equals, io.Discard)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
// Converts magnet URIs and info hashes into torrent metainfo files.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
_ "github.com/anacrolix/envpprof"
|
||||||
|
"github.com/anacrolix/tagflag"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/bencode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
args := struct {
|
||||||
|
tagflag.StartPos
|
||||||
|
Magnet []string
|
||||||
|
}{}
|
||||||
|
tagflag.Parse(&args)
|
||||||
|
cl, err := torrent.NewClient(nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error creating client: %s", err)
|
||||||
|
}
|
||||||
|
http.HandleFunc("/torrent", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cl.WriteStatus(w)
|
||||||
|
})
|
||||||
|
http.HandleFunc("/dht", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
for _, ds := range cl.DhtServers() {
|
||||||
|
ds.WriteStatus(w)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
for _, arg := range args.Magnet {
|
||||||
|
t, err := cl.AddMagnet(arg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error adding magnet to client: %s", err)
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
<-t.GotInfo()
|
||||||
|
mi := t.Metainfo()
|
||||||
|
t.Drop()
|
||||||
|
f, err := os.Create(t.Info().Name + ".torrent")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error creating torrent metainfo file: %s", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
err = bencode.NewEncoder(f).Encode(mi)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error writing torrent metainfo file: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
// Downloads torrents from the command-line.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
_ "net/http/pprof"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/anacrolix/envpprof"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/jessevdk/go-flags"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0])
|
||||||
|
|
||||||
|
func resolvedPeerAddrs(ss []string) (ret []torrent.PeerInfo, err error) {
|
||||||
|
for _, s := range ss {
|
||||||
|
var addr *net.TCPAddr
|
||||||
|
addr, err = net.ResolveTCPAddr("tcp", s)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ret = append(ret, torrent.PeerInfo{
|
||||||
|
Addr: addr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesCompleted(tc *torrent.Client) (ret int64) {
|
||||||
|
for _, t := range tc.Torrents() {
|
||||||
|
if t.Info() != nil {
|
||||||
|
ret += t.BytesCompleted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns an estimate of the total bytes for all torrents.
|
||||||
|
func totalBytesEstimate(tc *torrent.Client) (ret int64) {
|
||||||
|
var noInfo, hadInfo int64
|
||||||
|
for _, t := range tc.Torrents() {
|
||||||
|
info := t.Info()
|
||||||
|
if info == nil {
|
||||||
|
noInfo++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret += info.TotalLength()
|
||||||
|
hadInfo++
|
||||||
|
}
|
||||||
|
if hadInfo != 0 {
|
||||||
|
// Treat each torrent without info as the average of those with,
|
||||||
|
// rounded up.
|
||||||
|
ret += (noInfo*ret + hadInfo - 1) / hadInfo
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func progressLine(tc *torrent.Client) string {
|
||||||
|
return fmt.Sprintf("\033[K%s / %s\r", humanize.Bytes(uint64(bytesCompleted(tc))), humanize.Bytes(uint64(totalBytesEstimate(tc))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func dstFileName(picked string) string {
|
||||||
|
parts := strings.Split(picked, "/")
|
||||||
|
return parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
rootGroup := struct {
|
||||||
|
Client *torrent.ClientConfig `group:"Client Options"`
|
||||||
|
TestPeers []string `long:"test-peer" description:"address of peer to inject to every torrent"`
|
||||||
|
Pick string `long:"pick" description:"filename to pick"`
|
||||||
|
}{
|
||||||
|
Client: torrent.NewDefaultClientConfig(),
|
||||||
|
}
|
||||||
|
// Don't pass flags.PrintError because it's inconsistent with printing.
|
||||||
|
// https://github.com/jessevdk/go-flags/issues/132
|
||||||
|
parser := flags.NewParser(&rootGroup, flags.HelpFlag|flags.PassDoubleDash)
|
||||||
|
parser.Usage = "[OPTIONS] (magnet URI or .torrent file path)..."
|
||||||
|
posArgs, err := parser.Parse()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s", "Download from the BitTorrent network.\n\n")
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
log.Printf("File to pick: %s", rootGroup.Pick)
|
||||||
|
|
||||||
|
testPeers, err := resolvedPeerAddrs(rootGroup.TestPeers)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(posArgs) == 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, "no torrents specified")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpdir, err := os.MkdirTemp("", "torrent-pick-")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer os.RemoveAll(tmpdir)
|
||||||
|
|
||||||
|
rootGroup.Client.DataDir = tmpdir
|
||||||
|
|
||||||
|
client, err := torrent.NewClient(rootGroup.Client)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error creating client: %s", err)
|
||||||
|
}
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
client.WriteStatus(w)
|
||||||
|
})
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
dstName := dstFileName(rootGroup.Pick)
|
||||||
|
|
||||||
|
f, err := os.Create(dstName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
dstWriter := bufio.NewWriter(f)
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
for _, arg := range posArgs {
|
||||||
|
t := func() *torrent.Torrent {
|
||||||
|
if strings.HasPrefix(arg, "magnet:") {
|
||||||
|
t, err := client.AddMagnet(arg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error adding magnet: %s", err)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
} else {
|
||||||
|
metaInfo, err := metainfo.LoadFromFile(arg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
t, err := client.AddTorrent(metaInfo)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
t.AddPeers(testPeers)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
<-t.GotInfo()
|
||||||
|
for _, file := range t.Files() {
|
||||||
|
if file.DisplayPath() != rootGroup.Pick {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
file.Download()
|
||||||
|
srcReader := file.NewReader()
|
||||||
|
defer srcReader.Close()
|
||||||
|
io.Copy(dstWriter, srcReader)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Print("file not found")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
waitDone:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
break waitDone
|
||||||
|
case <-ticker.C:
|
||||||
|
os.Stdout.WriteString(progressLine(client))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rootGroup.Client.Seed {
|
||||||
|
select {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha1"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/anacrolix/tagflag"
|
||||||
|
"github.com/edsrzf/mmap-go"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/mmap_span"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mmapFile(name string) (mm storage.FileMapping, err error) {
|
||||||
|
f, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
fi, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if fi.Size() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reg, err := mmap.MapRegion(f, -1, mmap.RDONLY, mmap.COPY, 0)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return storage.WrapFileMapping(reg, f), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyTorrent(info *metainfo.Info, root string) error {
|
||||||
|
span := new(mmap_span.MMapSpan)
|
||||||
|
for _, file := range info.UpvertedFiles() {
|
||||||
|
filename := filepath.Join(append([]string{root, info.Name}, file.Path...)...)
|
||||||
|
mm, err := mmapFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if int64(len(mm.Bytes())) != file.Length {
|
||||||
|
return fmt.Errorf("file %q has wrong length", filename)
|
||||||
|
}
|
||||||
|
span.Append(mm)
|
||||||
|
}
|
||||||
|
span.InitIndex()
|
||||||
|
for i, numPieces := 0, info.NumPieces(); i < numPieces; i += 1 {
|
||||||
|
p := info.Piece(i)
|
||||||
|
hash := sha1.New()
|
||||||
|
_, err := io.Copy(hash, io.NewSectionReader(span, p.Offset(), p.Length()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
good := bytes.Equal(hash.Sum(nil), p.Hash().Bytes())
|
||||||
|
if !good {
|
||||||
|
return fmt.Errorf("hash mismatch at piece %d", i)
|
||||||
|
}
|
||||||
|
fmt.Printf("%d: %v: %v\n", i, p.Hash(), good)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(log.Flags() | log.Lshortfile)
|
||||||
|
flags := struct {
|
||||||
|
DataDir string
|
||||||
|
tagflag.StartPos
|
||||||
|
TorrentFile string
|
||||||
|
}{}
|
||||||
|
tagflag.Parse(&flags)
|
||||||
|
metaInfo, err := metainfo.LoadFromFile(flags.TorrentFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
info, err := metaInfo.UnmarshalInfo()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error unmarshalling info: %s", err)
|
||||||
|
}
|
||||||
|
err = verifyTorrent(&info, flags.DataDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("torrent failed verification: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/tracker"
|
||||||
|
"github.com/anacrolix/torrent/tracker/udp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AnnounceCmd struct {
|
||||||
|
Event udp.AnnounceEvent
|
||||||
|
Port *uint16
|
||||||
|
Tracker string `arg:"positional"`
|
||||||
|
InfoHash torrent.InfoHash `arg:"positional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func announceErr(flags AnnounceCmd) error {
|
||||||
|
req := tracker.AnnounceRequest{
|
||||||
|
InfoHash: flags.InfoHash,
|
||||||
|
Port: uint16(torrent.NewDefaultClientConfig().ListenPort),
|
||||||
|
NumWant: -1,
|
||||||
|
Event: flags.Event,
|
||||||
|
Left: -1,
|
||||||
|
}
|
||||||
|
if flags.Port != nil {
|
||||||
|
req.Port = *flags.Port
|
||||||
|
}
|
||||||
|
response, err := tracker.Announce{
|
||||||
|
TrackerUrl: flags.Tracker,
|
||||||
|
Request: req,
|
||||||
|
}.Do()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("doing announce: %w", err)
|
||||||
|
}
|
||||||
|
spew.Dump(response)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/anacrolix/bargle"
|
||||||
|
"github.com/anacrolix/tagflag"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/bencode"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
var builtinAnnounceList = [][]string{
|
||||||
|
{"http://p4p.arenabg.com:1337/announce"},
|
||||||
|
{"udp://tracker.opentrackr.org:1337/announce"},
|
||||||
|
{"udp://tracker.openbittorrent.com:6969/announce"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func create() (cmd bargle.Command) {
|
||||||
|
var args struct {
|
||||||
|
AnnounceList []string `name:"a" help:"extra announce-list tier entry"`
|
||||||
|
EmptyAnnounceList bool `name:"n" help:"exclude default announce-list entries"`
|
||||||
|
Comment string `name:"t" help:"comment"`
|
||||||
|
CreatedBy string `name:"c" help:"created by"`
|
||||||
|
InfoName *string `name:"i" help:"override info name (defaults to ROOT)"`
|
||||||
|
PieceLength tagflag.Bytes
|
||||||
|
Url []string `name:"u" help:"add webseed url"`
|
||||||
|
Private *bool
|
||||||
|
Root string `arg:"positional"`
|
||||||
|
}
|
||||||
|
cmd = bargle.FromStruct(&args)
|
||||||
|
cmd.Desc = "Creates a torrent metainfo for the file system rooted at ROOT, and outputs it to stdout"
|
||||||
|
cmd.DefaultAction = func() (err error) {
|
||||||
|
mi := metainfo.MetaInfo{
|
||||||
|
AnnounceList: builtinAnnounceList,
|
||||||
|
}
|
||||||
|
if args.EmptyAnnounceList {
|
||||||
|
mi.AnnounceList = make([][]string, 0)
|
||||||
|
}
|
||||||
|
for _, a := range args.AnnounceList {
|
||||||
|
mi.AnnounceList = append(mi.AnnounceList, []string{a})
|
||||||
|
}
|
||||||
|
mi.SetDefaults()
|
||||||
|
if len(args.Comment) > 0 {
|
||||||
|
mi.Comment = args.Comment
|
||||||
|
}
|
||||||
|
if len(args.CreatedBy) > 0 {
|
||||||
|
mi.CreatedBy = args.CreatedBy
|
||||||
|
}
|
||||||
|
mi.UrlList = args.Url
|
||||||
|
info := metainfo.Info{
|
||||||
|
PieceLength: args.PieceLength.Int64(),
|
||||||
|
Private: args.Private,
|
||||||
|
}
|
||||||
|
err = info.BuildFromFilePath(args.Root)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if args.InfoName != nil {
|
||||||
|
info.Name = *args.InfoName
|
||||||
|
}
|
||||||
|
mi.InfoBytes, err = bencode.Marshal(info)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = mi.Write(os.Stdout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,394 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"expvar"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anacrolix/log"
|
||||||
|
"github.com/anacrolix/tagflag"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/iplist"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
pp "github.com/anacrolix/torrent/peer_protocol"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func torrentBar(t *torrent.Torrent, pieceStates bool) {
|
||||||
|
go func() {
|
||||||
|
start := time.Now()
|
||||||
|
if t.Info() == nil {
|
||||||
|
fmt.Printf("%v: getting torrent info for %q\n", time.Since(start), t.Name())
|
||||||
|
<-t.GotInfo()
|
||||||
|
}
|
||||||
|
lastStats := t.Stats()
|
||||||
|
var lastLine string
|
||||||
|
interval := 3 * time.Second
|
||||||
|
for range time.Tick(interval) {
|
||||||
|
var completedPieces, partialPieces int
|
||||||
|
psrs := t.PieceStateRuns()
|
||||||
|
for _, r := range psrs {
|
||||||
|
if r.Complete {
|
||||||
|
completedPieces += r.Length
|
||||||
|
}
|
||||||
|
if r.Partial {
|
||||||
|
partialPieces += r.Length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stats := t.Stats()
|
||||||
|
byteRate := int64(time.Second)
|
||||||
|
byteRate *= stats.BytesReadUsefulData.Int64() - lastStats.BytesReadUsefulData.Int64()
|
||||||
|
byteRate /= int64(interval)
|
||||||
|
line := fmt.Sprintf(
|
||||||
|
"%v: downloading %q: %s/%s, %d/%d pieces completed (%d partial): %v/s\n",
|
||||||
|
time.Since(start),
|
||||||
|
t.Name(),
|
||||||
|
humanize.Bytes(uint64(t.BytesCompleted())),
|
||||||
|
humanize.Bytes(uint64(t.Length())),
|
||||||
|
completedPieces,
|
||||||
|
t.NumPieces(),
|
||||||
|
partialPieces,
|
||||||
|
humanize.Bytes(uint64(byteRate)),
|
||||||
|
)
|
||||||
|
if line != lastLine {
|
||||||
|
lastLine = line
|
||||||
|
os.Stdout.WriteString(line)
|
||||||
|
}
|
||||||
|
if pieceStates {
|
||||||
|
fmt.Println(psrs)
|
||||||
|
}
|
||||||
|
lastStats = stats
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
type stringAddr string
|
||||||
|
|
||||||
|
func (stringAddr) Network() string { return "" }
|
||||||
|
func (me stringAddr) String() string { return string(me) }
|
||||||
|
|
||||||
|
func resolveTestPeers(addrs []string) (ret []torrent.PeerInfo) {
|
||||||
|
for _, ta := range addrs {
|
||||||
|
ret = append(ret, torrent.PeerInfo{
|
||||||
|
Addr: stringAddr(ta),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func addTorrents(ctx context.Context, client *torrent.Client, flags downloadFlags, wg *sync.WaitGroup) error {
|
||||||
|
testPeers := resolveTestPeers(flags.TestPeer)
|
||||||
|
for _, arg := range flags.Torrent {
|
||||||
|
t, err := func() (*torrent.Torrent, error) {
|
||||||
|
if strings.HasPrefix(arg, "magnet:") {
|
||||||
|
t, err := client.AddMagnet(arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error adding magnet: %w", err)
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
} else if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
|
||||||
|
response, err := http.Get(arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error downloading torrent file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
metaInfo, err := metainfo.Load(response.Body)
|
||||||
|
defer response.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err)
|
||||||
|
}
|
||||||
|
t, err := client.AddTorrent(metaInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("adding torrent: %w", err)
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
} else if strings.HasPrefix(arg, "infohash:") {
|
||||||
|
t, _ := client.AddTorrentInfoHash(metainfo.NewHashFromHex(strings.TrimPrefix(arg, "infohash:")))
|
||||||
|
return t, nil
|
||||||
|
} else {
|
||||||
|
metaInfo, err := metainfo.LoadFromFile(arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err)
|
||||||
|
}
|
||||||
|
t, err := client.AddTorrent(metaInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("adding torrent: %w", err)
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("adding torrent for %q: %w", arg, err)
|
||||||
|
}
|
||||||
|
if flags.Progress {
|
||||||
|
torrentBar(t, flags.PieceStates)
|
||||||
|
}
|
||||||
|
t.AddPeers(testPeers)
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-t.GotInfo():
|
||||||
|
}
|
||||||
|
if flags.SaveMetainfos {
|
||||||
|
path := fmt.Sprintf("%v.torrent", t.InfoHash().HexString())
|
||||||
|
err := writeMetainfoToFile(t.Metainfo(), path)
|
||||||
|
if err == nil {
|
||||||
|
log.Printf("wrote %q", path)
|
||||||
|
} else {
|
||||||
|
log.Printf("error writing %q: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(flags.File) == 0 {
|
||||||
|
t.DownloadAll()
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
waitForPieces(ctx, t, 0, t.NumPieces())
|
||||||
|
}()
|
||||||
|
if flags.LinearDiscard {
|
||||||
|
r := t.NewReader()
|
||||||
|
io.Copy(io.Discard, r)
|
||||||
|
r.Close()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, f := range t.Files() {
|
||||||
|
for _, fileArg := range flags.File {
|
||||||
|
if f.DisplayPath() == fileArg {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
waitForPieces(ctx, t, f.BeginPieceIndex(), f.EndPieceIndex())
|
||||||
|
}()
|
||||||
|
f.Download()
|
||||||
|
if flags.LinearDiscard {
|
||||||
|
r := f.NewReader()
|
||||||
|
go func() {
|
||||||
|
defer r.Close()
|
||||||
|
io.Copy(io.Discard, r)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForPieces(ctx context.Context, t *torrent.Torrent, beginIndex, endIndex int) {
|
||||||
|
sub := t.SubscribePieceStateChanges()
|
||||||
|
defer sub.Close()
|
||||||
|
expected := storage.Completion{
|
||||||
|
Complete: true,
|
||||||
|
Ok: true,
|
||||||
|
}
|
||||||
|
pending := make(map[int]struct{})
|
||||||
|
for i := beginIndex; i < endIndex; i++ {
|
||||||
|
if t.Piece(i).State().Completion != expected {
|
||||||
|
pending[i] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
if len(pending) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case ev := <-sub.Values:
|
||||||
|
if ev.Completion == expected {
|
||||||
|
delete(pending, ev.Index)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeMetainfoToFile(mi metainfo.MetaInfo, path string) error {
|
||||||
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o640)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
err = mi.Write(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type downloadFlags struct {
|
||||||
|
Debug bool
|
||||||
|
DownloadCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
type DownloadCmd struct {
|
||||||
|
SaveMetainfos bool
|
||||||
|
Mmap bool `help:"memory-map torrent data"`
|
||||||
|
Seed bool `help:"seed after download is complete"`
|
||||||
|
Addr string `help:"network listen addr"`
|
||||||
|
MaxUnverifiedBytes *tagflag.Bytes `help:"maximum number bytes to have pending verification"`
|
||||||
|
UploadRate *tagflag.Bytes `help:"max piece bytes to send per second"`
|
||||||
|
DownloadRate *tagflag.Bytes `help:"max bytes per second down from peers"`
|
||||||
|
PackedBlocklist string
|
||||||
|
PublicIP net.IP
|
||||||
|
Progress bool `default:"true"`
|
||||||
|
PieceStates bool `help:"Output piece state runs at progress intervals."`
|
||||||
|
Quiet bool `help:"discard client logging"`
|
||||||
|
Stats bool `help:"print stats at termination"`
|
||||||
|
Dht bool `default:"true"`
|
||||||
|
PortForward bool `default:"true"`
|
||||||
|
|
||||||
|
TcpPeers bool `default:"true"`
|
||||||
|
UtpPeers bool `default:"true"`
|
||||||
|
Webtorrent bool `default:"true"`
|
||||||
|
DisableWebseeds bool
|
||||||
|
// Don't progress past handshake for peer connections where the peer doesn't offer the fast
|
||||||
|
// extension.
|
||||||
|
RequireFastExtension bool
|
||||||
|
|
||||||
|
Ipv4 bool `default:"true"`
|
||||||
|
Ipv6 bool `default:"true"`
|
||||||
|
Pex bool `default:"true"`
|
||||||
|
|
||||||
|
LinearDiscard bool `help:"Read and discard selected regions from start to finish. Useful for testing simultaneous Reader and static file prioritization."`
|
||||||
|
TestPeer []string `help:"addresses of some starting peers"`
|
||||||
|
|
||||||
|
File []string
|
||||||
|
Torrent []string `arity:"+" help:"torrent file path or magnet uri" arg:"positional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func statsEnabled(flags downloadFlags) bool {
|
||||||
|
return flags.Stats
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadErr(flags downloadFlags) error {
|
||||||
|
clientConfig := torrent.NewDefaultClientConfig()
|
||||||
|
clientConfig.DisableWebseeds = flags.DisableWebseeds
|
||||||
|
clientConfig.DisableTCP = !flags.TcpPeers
|
||||||
|
clientConfig.DisableUTP = !flags.UtpPeers
|
||||||
|
clientConfig.DisableIPv4 = !flags.Ipv4
|
||||||
|
clientConfig.DisableIPv6 = !flags.Ipv6
|
||||||
|
clientConfig.DisableAcceptRateLimiting = true
|
||||||
|
clientConfig.NoDHT = !flags.Dht
|
||||||
|
clientConfig.Debug = flags.Debug
|
||||||
|
clientConfig.Seed = flags.Seed
|
||||||
|
clientConfig.PublicIp4 = flags.PublicIP.To4()
|
||||||
|
clientConfig.PublicIp6 = flags.PublicIP
|
||||||
|
clientConfig.DisablePEX = !flags.Pex
|
||||||
|
clientConfig.DisableWebtorrent = !flags.Webtorrent
|
||||||
|
clientConfig.NoDefaultPortForwarding = !flags.PortForward
|
||||||
|
if flags.PackedBlocklist != "" {
|
||||||
|
blocklist, err := iplist.MMapPackedFile(flags.PackedBlocklist)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading blocklist: %v", err)
|
||||||
|
}
|
||||||
|
defer blocklist.Close()
|
||||||
|
clientConfig.IPBlocklist = blocklist
|
||||||
|
}
|
||||||
|
if flags.Mmap {
|
||||||
|
clientConfig.DefaultStorage = storage.NewMMap("")
|
||||||
|
}
|
||||||
|
if flags.Addr != "" {
|
||||||
|
clientConfig.SetListenAddr(flags.Addr)
|
||||||
|
}
|
||||||
|
if flags.UploadRate != nil {
|
||||||
|
// TODO: I think the upload rate limit could be much lower.
|
||||||
|
clientConfig.UploadRateLimiter = rate.NewLimiter(rate.Limit(*flags.UploadRate), 256<<10)
|
||||||
|
}
|
||||||
|
if flags.DownloadRate != nil {
|
||||||
|
clientConfig.DownloadRateLimiter = rate.NewLimiter(rate.Limit(*flags.DownloadRate), 1<<16)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
logger := log.Default.WithNames("main", "client")
|
||||||
|
if flags.Quiet {
|
||||||
|
logger = logger.FilterLevel(log.Critical)
|
||||||
|
}
|
||||||
|
clientConfig.Logger = logger
|
||||||
|
}
|
||||||
|
if flags.RequireFastExtension {
|
||||||
|
clientConfig.MinPeerExtensions.SetBit(pp.ExtensionBitFast, true)
|
||||||
|
}
|
||||||
|
if flags.MaxUnverifiedBytes != nil {
|
||||||
|
clientConfig.MaxUnverifiedBytes = flags.MaxUnverifiedBytes.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := torrent.NewClient(clientConfig)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating client: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Write status on the root path on the default HTTP muxer. This will be bound to localhost
|
||||||
|
// somewhere if GOPPROF is set, thanks to the envpprof import.
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
client.WriteStatus(w)
|
||||||
|
})
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
err = addTorrents(ctx, client, flags, &wg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("adding torrents: %w", err)
|
||||||
|
}
|
||||||
|
started := time.Now()
|
||||||
|
defer outputStats(client, flags)
|
||||||
|
wg.Wait()
|
||||||
|
if ctx.Err() == nil {
|
||||||
|
log.Print("downloaded ALL the torrents")
|
||||||
|
} else {
|
||||||
|
err = ctx.Err()
|
||||||
|
}
|
||||||
|
clientConnStats := client.ConnStats()
|
||||||
|
log.Printf(
|
||||||
|
"average download rate: %v/s",
|
||||||
|
humanize.Bytes(uint64(float64(
|
||||||
|
clientConnStats.BytesReadUsefulData.Int64(),
|
||||||
|
)/time.Since(started).Seconds())),
|
||||||
|
)
|
||||||
|
if flags.Seed {
|
||||||
|
if len(client.Torrents()) == 0 {
|
||||||
|
log.Print("no torrents to seed")
|
||||||
|
} else {
|
||||||
|
outputStats(client, flags)
|
||||||
|
<-ctx.Done()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spew.Dump(expvar.Get("torrent").(*expvar.Map).Get("chunks received"))
|
||||||
|
spew.Dump(client.ConnStats())
|
||||||
|
clStats := client.ConnStats()
|
||||||
|
sentOverhead := clStats.BytesWritten.Int64() - clStats.BytesWrittenData.Int64()
|
||||||
|
log.Printf(
|
||||||
|
"client read %v, %.1f%% was useful data. sent %v non-data bytes",
|
||||||
|
humanize.Bytes(uint64(clStats.BytesRead.Int64())),
|
||||||
|
100*float64(clStats.BytesReadUsefulData.Int64())/float64(clStats.BytesRead.Int64()),
|
||||||
|
humanize.Bytes(uint64(sentOverhead)))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func outputStats(cl *torrent.Client, args downloadFlags) {
|
||||||
|
if !statsEnabled(args) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expvar.Do(func(kv expvar.KeyValue) {
|
||||||
|
fmt.Printf("%s: %s\n", kv.Key, kv.Value)
|
||||||
|
})
|
||||||
|
cl.WriteStatus(os.Stdout)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
// Downloads torrents from the command-line.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
stdLog "log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anacrolix/bargle"
|
||||||
|
"github.com/anacrolix/envpprof"
|
||||||
|
"github.com/anacrolix/log"
|
||||||
|
xprometheus "github.com/anacrolix/missinggo/v2/prometheus"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||||
|
"go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/bencode"
|
||||||
|
"github.com/anacrolix/torrent/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
prometheus.MustRegister(xprometheus.NewExpvarCollector())
|
||||||
|
http.Handle("/metrics", promhttp.Handler())
|
||||||
|
}
|
||||||
|
|
||||||
|
func shutdownTracerProvider(ctx context.Context, tp *trace.TracerProvider) {
|
||||||
|
started := time.Now()
|
||||||
|
err := tp.Shutdown(ctx)
|
||||||
|
elapsed := time.Since(started)
|
||||||
|
log.Levelf(log.Error, "shutting down tracer provider (took %v): %v", elapsed, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
defer stdLog.SetFlags(stdLog.Flags() | stdLog.Lshortfile)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
tracingExporter, err := otlptracegrpc.New(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("creating tracing exporter: %v", err)
|
||||||
|
}
|
||||||
|
tracerProvider := trace.NewTracerProvider(trace.WithBatcher(tracingExporter))
|
||||||
|
defer shutdownTracerProvider(ctx, tracerProvider)
|
||||||
|
otel.SetTracerProvider(tracerProvider)
|
||||||
|
|
||||||
|
main := bargle.Main{}
|
||||||
|
main.Defer(envpprof.Stop)
|
||||||
|
main.Defer(func() { shutdownTracerProvider(ctx, tracerProvider) })
|
||||||
|
debug := false
|
||||||
|
debugFlag := bargle.NewFlag(&debug)
|
||||||
|
debugFlag.AddLong("debug")
|
||||||
|
main.Options = append(main.Options, debugFlag.Make())
|
||||||
|
main.Positionals = append(main.Positionals,
|
||||||
|
bargle.Subcommand{Name: "metainfo", Command: metainfoCmd()},
|
||||||
|
bargle.Subcommand{Name: "announce", Command: func() bargle.Command {
|
||||||
|
var ac AnnounceCmd
|
||||||
|
cmd := bargle.FromStruct(&ac)
|
||||||
|
cmd.DefaultAction = func() error {
|
||||||
|
return announceErr(ac)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}()},
|
||||||
|
bargle.Subcommand{Name: "scrape", Command: func() bargle.Command {
|
||||||
|
var scrapeCfg scrapeCfg
|
||||||
|
cmd := bargle.FromStruct(&scrapeCfg)
|
||||||
|
cmd.Desc = "fetch swarm metrics for info-hashes from tracker"
|
||||||
|
cmd.DefaultAction = func() error {
|
||||||
|
return scrape(scrapeCfg)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}()},
|
||||||
|
bargle.Subcommand{Name: "download", Command: func() bargle.Command {
|
||||||
|
var dlc DownloadCmd
|
||||||
|
cmd := bargle.FromStruct(&dlc)
|
||||||
|
cmd.DefaultAction = func() error {
|
||||||
|
return downloadErr(downloadFlags{
|
||||||
|
Debug: debug,
|
||||||
|
DownloadCmd: dlc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}()},
|
||||||
|
bargle.Subcommand{
|
||||||
|
Name: "bencode",
|
||||||
|
Command: func() (cmd bargle.Command) {
|
||||||
|
var print func(interface{}) error
|
||||||
|
cmd.Positionals = append(cmd.Positionals,
|
||||||
|
bargle.Subcommand{Name: "json", Command: func() (cmd bargle.Command) {
|
||||||
|
cmd.DefaultAction = func() error {
|
||||||
|
je := json.NewEncoder(os.Stdout)
|
||||||
|
je.SetIndent("", " ")
|
||||||
|
print = je.Encode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}()},
|
||||||
|
bargle.Subcommand{Name: "spew", Command: func() (cmd bargle.Command) {
|
||||||
|
cmd.DefaultAction = func() error {
|
||||||
|
config := spew.NewDefaultConfig()
|
||||||
|
config.DisableCapacities = true
|
||||||
|
config.Indent = " "
|
||||||
|
print = func(v interface{}) error {
|
||||||
|
config.Dump(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}()})
|
||||||
|
d := bencode.NewDecoder(os.Stdin)
|
||||||
|
cmd.AfterParseFunc = func(ctx bargle.Context) error {
|
||||||
|
ctx.AfterParse(func() error {
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
var v interface{}
|
||||||
|
err := d.Decode(&v)
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decoding message index %d: %w", i, err)
|
||||||
|
}
|
||||||
|
print(v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cmd.Desc = "reads bencoding from stdin into Go native types and spews the result"
|
||||||
|
return
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
bargle.Subcommand{Name: "version", Command: bargle.Command{
|
||||||
|
DefaultAction: func() error {
|
||||||
|
fmt.Printf("HTTP User-Agent: %q\n", version.DefaultHttpUserAgent)
|
||||||
|
fmt.Printf("Torrent client version: %q\n", version.DefaultExtendedHandshakeClientVersion)
|
||||||
|
fmt.Printf("Torrent version prefix: %q\n", version.DefaultBep20Prefix)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Desc: "prints various protocol default version strings",
|
||||||
|
}},
|
||||||
|
bargle.Subcommand{Name: "serve", Command: serve()},
|
||||||
|
bargle.Subcommand{Name: "create", Command: create()},
|
||||||
|
)
|
||||||
|
main.Run()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anacrolix/bargle"
|
||||||
|
"github.com/bradfitz/iter"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pprintMetainfoFlags struct {
|
||||||
|
JustName bool
|
||||||
|
PieceHashes bool
|
||||||
|
Files bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func metainfoCmd() (cmd bargle.Command) {
|
||||||
|
var metainfoPath string
|
||||||
|
var mi *metainfo.MetaInfo
|
||||||
|
// TODO: Test if bargle treats no subcommand as a failure.
|
||||||
|
cmd.Positionals = append(cmd.Positionals,
|
||||||
|
&bargle.Positional{
|
||||||
|
Name: "torrent file",
|
||||||
|
Value: &bargle.String{Target: &metainfoPath},
|
||||||
|
AfterParseFunc: func(ctx bargle.Context) error {
|
||||||
|
ctx.AfterParse(func() (err error) {
|
||||||
|
if strings.HasPrefix(metainfoPath, "http://") || strings.HasPrefix(metainfoPath, "https://") {
|
||||||
|
response, err := http.Get(metainfoPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mi, err = metainfo.Load(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mi, err = metainfo.LoadFromFile(metainfoPath)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bargle.Subcommand{Name: "magnet", Command: func() (cmd bargle.Command) {
|
||||||
|
cmd.DefaultAction = func() (err error) {
|
||||||
|
info, err := mi.UnmarshalInfo()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stdout, "%s\n", mi.Magnet(nil, &info).String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}()},
|
||||||
|
bargle.Subcommand{Name: "pprint", Command: func() (cmd bargle.Command) {
|
||||||
|
var flags pprintMetainfoFlags
|
||||||
|
cmd = bargle.FromStruct(&flags)
|
||||||
|
cmd.DefaultAction = func() (err error) {
|
||||||
|
err = pprintMetainfo(mi, flags)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !flags.JustName {
|
||||||
|
os.Stdout.WriteString("\n")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}()},
|
||||||
|
//bargle.Subcommand{Name: "infohash", Command: func(ctx args.SubCmdCtx) (err error) {
|
||||||
|
// fmt.Printf("%s: %s\n", mi.HashInfoBytes().HexString(), metainfoPath)
|
||||||
|
// return nil
|
||||||
|
//}},
|
||||||
|
//bargle.Subcommand{Name: "list-files", Command: func(ctx args.SubCmdCtx) (err error) {
|
||||||
|
// info, err := mi.UnmarshalInfo()
|
||||||
|
// if err != nil {
|
||||||
|
// return fmt.Errorf("unmarshalling info from metainfo at %q: %v", metainfoPath, err)
|
||||||
|
// }
|
||||||
|
// for _, f := range info.UpvertedFiles() {
|
||||||
|
// fmt.Println(f.DisplayPath(&info))
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
//}},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func pprintMetainfo(metainfo *metainfo.MetaInfo, flags pprintMetainfoFlags) error {
|
||||||
|
info, err := metainfo.UnmarshalInfo()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error unmarshalling info: %s", err)
|
||||||
|
}
|
||||||
|
if flags.JustName {
|
||||||
|
fmt.Printf("%s\n", info.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
d := map[string]interface{}{
|
||||||
|
"Name": info.Name,
|
||||||
|
"Name.Utf8": info.NameUtf8,
|
||||||
|
"NumPieces": info.NumPieces(),
|
||||||
|
"PieceLength": info.PieceLength,
|
||||||
|
"InfoHash": metainfo.HashInfoBytes().HexString(),
|
||||||
|
"NumFiles": len(info.UpvertedFiles()),
|
||||||
|
"TotalLength": info.TotalLength(),
|
||||||
|
"Announce": metainfo.Announce,
|
||||||
|
"AnnounceList": metainfo.AnnounceList,
|
||||||
|
"UrlList": metainfo.UrlList,
|
||||||
|
}
|
||||||
|
if len(metainfo.Nodes) > 0 {
|
||||||
|
d["Nodes"] = metainfo.Nodes
|
||||||
|
}
|
||||||
|
if flags.Files {
|
||||||
|
d["Files"] = info.UpvertedFiles()
|
||||||
|
}
|
||||||
|
if flags.PieceHashes {
|
||||||
|
d["PieceHashes"] = func() (ret []string) {
|
||||||
|
for i := range iter.N(info.NumPieces()) {
|
||||||
|
ret = append(ret, hex.EncodeToString(info.Pieces[i*20:(i+1)*20]))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
b, _ := json.MarshalIndent(d, "", " ")
|
||||||
|
_, err = os.Stdout.Write(b)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/tracker"
|
||||||
|
)
|
||||||
|
|
||||||
|
type scrapeCfg struct {
|
||||||
|
Tracker string `arg:"positional"`
|
||||||
|
InfoHashes []torrent.InfoHash `arity:"+" arg:"positional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrape(flags scrapeCfg) error {
|
||||||
|
cc, err := tracker.NewClient(flags.Tracker, tracker.NewClientOpts{})
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("creating new tracker client: %w", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cc.Close()
|
||||||
|
scrapeOut, err := cc.Scrape(context.TODO(), flags.InfoHashes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("scraping: %w", err)
|
||||||
|
}
|
||||||
|
spew.Dump(scrapeOut)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/anacrolix/bargle"
|
||||||
|
"github.com/anacrolix/log"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/bencode"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func serve() (cmd bargle.Command) {
|
||||||
|
var filePaths []string
|
||||||
|
cmd.Positionals = append(cmd.Positionals, &bargle.Positional{
|
||||||
|
Value: bargle.AutoUnmarshaler(&filePaths),
|
||||||
|
})
|
||||||
|
cmd.Desc = "creates and seeds a torrent from a filepath"
|
||||||
|
cmd.DefaultAction = func() error {
|
||||||
|
cfg := torrent.NewDefaultClientConfig()
|
||||||
|
cfg.Seed = true
|
||||||
|
cl, err := torrent.NewClient(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("new torrent client: %w", err)
|
||||||
|
}
|
||||||
|
defer cl.Close()
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cl.WriteStatus(w)
|
||||||
|
})
|
||||||
|
for _, filePath := range filePaths {
|
||||||
|
totalLength, err := totalLength(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("calculating total length of %q: %v", filePath, err)
|
||||||
|
}
|
||||||
|
pieceLength := metainfo.ChoosePieceLength(totalLength)
|
||||||
|
info := metainfo.Info{
|
||||||
|
PieceLength: pieceLength,
|
||||||
|
}
|
||||||
|
err = info.BuildFromFilePath(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("building info from path %q: %w", filePath, err)
|
||||||
|
}
|
||||||
|
for _, fi := range info.Files {
|
||||||
|
log.Printf("added %q", fi.Path)
|
||||||
|
}
|
||||||
|
mi := metainfo.MetaInfo{
|
||||||
|
InfoBytes: bencode.MustMarshal(info),
|
||||||
|
}
|
||||||
|
pc, err := storage.NewDefaultPieceCompletionForDir(".")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("new piece completion: %w", err)
|
||||||
|
}
|
||||||
|
defer pc.Close()
|
||||||
|
ih := mi.HashInfoBytes()
|
||||||
|
to, _ := cl.AddTorrentOpt(torrent.AddTorrentOpts{
|
||||||
|
InfoHash: ih,
|
||||||
|
Storage: storage.NewFileOpts(storage.NewFileClientOpts{
|
||||||
|
ClientBaseDir: filePath,
|
||||||
|
FilePathMaker: func(opts storage.FilePathMakerOpts) string {
|
||||||
|
return filepath.Join(opts.File.Path...)
|
||||||
|
},
|
||||||
|
TorrentDirMaker: nil,
|
||||||
|
PieceCompletion: pc,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
defer to.Drop()
|
||||||
|
err = to.MergeSpec(&torrent.TorrentSpec{
|
||||||
|
InfoBytes: mi.InfoBytes,
|
||||||
|
Trackers: [][]string{{
|
||||||
|
`wss://tracker.btorrent.xyz`,
|
||||||
|
`wss://tracker.openwebtorrent.com`,
|
||||||
|
"http://p4p.arenabg.com:1337/announce",
|
||||||
|
"udp://tracker.opentrackr.org:1337/announce",
|
||||||
|
"udp://tracker.openbittorrent.com:6969/announce",
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting trackers: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%v: %v\n", to, to.Metainfo().Magnet(&ih, &info))
|
||||||
|
}
|
||||||
|
select {}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func totalLength(path string) (totalLength int64, err error) {
|
||||||
|
err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
totalLength += info.Size()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("walking path, %w", err)
|
||||||
|
}
|
||||||
|
return totalLength, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/segments"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LengthIterFromUpvertedFiles(fis []metainfo.FileInfo) segments.LengthIter {
|
||||||
|
i := 0
|
||||||
|
return func() (segments.Length, bool) {
|
||||||
|
if i == len(fis) {
|
||||||
|
return -1, false
|
||||||
|
}
|
||||||
|
l := fis[i].Length
|
||||||
|
i++
|
||||||
|
return l, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anacrolix/dht/v2"
|
||||||
|
"github.com/anacrolix/dht/v2/krpc"
|
||||||
|
"github.com/anacrolix/log"
|
||||||
|
"github.com/anacrolix/missinggo/v2"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/iplist"
|
||||||
|
"github.com/anacrolix/torrent/mse"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
"github.com/anacrolix/torrent/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Contains config elements that are exclusive to tracker handling. There may be other fields in
|
||||||
|
// ClientConfig that are also relevant.
|
||||||
|
type ClientTrackerConfig struct {
|
||||||
|
// Don't announce to trackers. This only leaves DHT to discover peers.
|
||||||
|
DisableTrackers bool `long:"disable-trackers"`
|
||||||
|
// Defines DialContext func to use for HTTP tracker announcements
|
||||||
|
TrackerDialContext func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||||
|
// Defines ListenPacket func to use for UDP tracker announcements
|
||||||
|
TrackerListenPacket func(network, addr string) (net.PacketConn, error)
|
||||||
|
// Takes a tracker's hostname and requests DNS A and AAAA records.
|
||||||
|
// Used in case DNS lookups require a special setup (i.e., dns-over-https)
|
||||||
|
LookupTrackerIp func(*url.URL) ([]net.IP, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientDhtConfig struct {
|
||||||
|
// Don't create a DHT.
|
||||||
|
NoDHT bool `long:"disable-dht"`
|
||||||
|
DhtStartingNodes func(network string) dht.StartingNodesGetter
|
||||||
|
// Called for each anacrolix/dht Server created for the Client.
|
||||||
|
ConfigureAnacrolixDhtServer func(*dht.ServerConfig)
|
||||||
|
PeriodicallyAnnounceTorrentsToDht bool
|
||||||
|
// OnQuery hook func
|
||||||
|
DHTOnQuery func(query *krpc.Msg, source net.Addr) (propagate bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probably not safe to modify this after it's given to a Client.
|
||||||
|
type ClientConfig struct {
|
||||||
|
ClientTrackerConfig
|
||||||
|
ClientDhtConfig
|
||||||
|
|
||||||
|
// Store torrent file data in this directory unless .DefaultStorage is
|
||||||
|
// specified.
|
||||||
|
DataDir string `long:"data-dir" description:"directory to store downloaded torrent data"`
|
||||||
|
// The address to listen for new uTP and TCP BitTorrent protocol connections. DHT shares a UDP
|
||||||
|
// socket with uTP unless configured otherwise.
|
||||||
|
ListenHost func(network string) string
|
||||||
|
ListenPort int
|
||||||
|
NoDefaultPortForwarding bool
|
||||||
|
UpnpID string
|
||||||
|
DisablePEX bool `long:"disable-pex"`
|
||||||
|
|
||||||
|
// Never send chunks to peers.
|
||||||
|
NoUpload bool `long:"no-upload"`
|
||||||
|
// Disable uploading even when it isn't fair.
|
||||||
|
DisableAggressiveUpload bool `long:"disable-aggressive-upload"`
|
||||||
|
// Upload even after there's nothing in it for us. By default uploading is
|
||||||
|
// not altruistic, we'll only upload to encourage the peer to reciprocate.
|
||||||
|
Seed bool `long:"seed"`
|
||||||
|
// Only applies to chunks uploaded to peers, to maintain responsiveness
|
||||||
|
// communicating local Client state to peers. Each limiter token
|
||||||
|
// represents one byte. The Limiter's burst must be large enough to fit a
|
||||||
|
// whole chunk, which is usually 16 KiB (see TorrentSpec.ChunkSize).
|
||||||
|
UploadRateLimiter *rate.Limiter
|
||||||
|
// Rate limits all reads from connections to peers. Each limiter token
|
||||||
|
// represents one byte. The Limiter's burst must be bigger than the
|
||||||
|
// largest Read performed on a the underlying rate-limiting io.Reader
|
||||||
|
// minus one. This is likely to be the larger of the main read loop buffer
|
||||||
|
// (~4096), and the requested chunk size (~16KiB, see
|
||||||
|
// TorrentSpec.ChunkSize).
|
||||||
|
DownloadRateLimiter *rate.Limiter
|
||||||
|
// Maximum unverified bytes across all torrents. Not used if zero.
|
||||||
|
MaxUnverifiedBytes int64
|
||||||
|
|
||||||
|
// User-provided Client peer ID. If not present, one is generated automatically.
|
||||||
|
PeerID string
|
||||||
|
// For the bittorrent protocol.
|
||||||
|
DisableUTP bool
|
||||||
|
// For the bittorrent protocol.
|
||||||
|
DisableTCP bool `long:"disable-tcp"`
|
||||||
|
// Called to instantiate storage for each added torrent. Builtin backends
|
||||||
|
// are in the storage package. If not set, the "file" implementation is
|
||||||
|
// used (and Closed when the Client is Closed).
|
||||||
|
DefaultStorage storage.ClientImpl
|
||||||
|
|
||||||
|
HeaderObfuscationPolicy HeaderObfuscationPolicy
|
||||||
|
// The crypto methods to offer when initiating connections with header obfuscation.
|
||||||
|
CryptoProvides mse.CryptoMethod
|
||||||
|
// Chooses the crypto method to use when receiving connections with header obfuscation.
|
||||||
|
CryptoSelector mse.CryptoSelector
|
||||||
|
|
||||||
|
IPBlocklist iplist.Ranger
|
||||||
|
DisableIPv6 bool `long:"disable-ipv6"`
|
||||||
|
DisableIPv4 bool
|
||||||
|
DisableIPv4Peers bool
|
||||||
|
// Perform logging and any other behaviour that will help debug.
|
||||||
|
Debug bool `help:"enable debugging"`
|
||||||
|
Logger log.Logger
|
||||||
|
|
||||||
|
// Used for torrent sources and webseeding if set.
|
||||||
|
WebTransport http.RoundTripper
|
||||||
|
// Defines proxy for HTTP requests, such as for trackers. It's commonly set from the result of
|
||||||
|
// "net/http".ProxyURL(HTTPProxy).
|
||||||
|
HTTPProxy func(*http.Request) (*url.URL, error)
|
||||||
|
// Defines DialContext func to use for HTTP requests, such as for fetching metainfo and webtorrent seeds
|
||||||
|
HTTPDialContext func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||||
|
// HTTPUserAgent changes default UserAgent for HTTP requests
|
||||||
|
HTTPUserAgent string
|
||||||
|
// HttpRequestDirector modifies the request before it's sent.
|
||||||
|
// Useful for adding authentication headers, for example
|
||||||
|
HttpRequestDirector func(*http.Request) error
|
||||||
|
// WebsocketTrackerHttpHeader returns a custom header to be used when dialing a websocket connection
|
||||||
|
// to the tracker. Useful for adding authentication headers
|
||||||
|
WebsocketTrackerHttpHeader func() http.Header
|
||||||
|
// Updated occasionally to when there's been some changes to client
|
||||||
|
// behaviour in case other clients are assuming anything of us. See also
|
||||||
|
// `bep20`.
|
||||||
|
ExtendedHandshakeClientVersion string
|
||||||
|
// Peer ID client identifier prefix. We'll update this occasionally to
|
||||||
|
// reflect changes to client behaviour that other clients may depend on.
|
||||||
|
// Also see `extendedHandshakeClientVersion`.
|
||||||
|
Bep20 string
|
||||||
|
|
||||||
|
// Peer dial timeout to use when there are limited peers.
|
||||||
|
NominalDialTimeout time.Duration
|
||||||
|
// Minimum peer dial timeout to use (even if we have lots of peers).
|
||||||
|
MinDialTimeout time.Duration
|
||||||
|
EstablishedConnsPerTorrent int
|
||||||
|
HalfOpenConnsPerTorrent int
|
||||||
|
TotalHalfOpenConns int
|
||||||
|
// Maximum number of peer addresses in reserve.
|
||||||
|
TorrentPeersHighWater int
|
||||||
|
// Minumum number of peers before effort is made to obtain more peers.
|
||||||
|
TorrentPeersLowWater int
|
||||||
|
|
||||||
|
// Limit how long handshake can take. This is to reduce the lingering
|
||||||
|
// impact of a few bad apples. 4s loses 1% of successful handshakes that
|
||||||
|
// are obtained with 60s timeout, and 5% of unsuccessful handshakes.
|
||||||
|
HandshakesTimeout time.Duration
|
||||||
|
// How long between writes before sending a keep alive message on a peer connection that we want
|
||||||
|
// to maintain.
|
||||||
|
KeepAliveTimeout time.Duration
|
||||||
|
// Maximum bytes to buffer per peer connection for peer request data before it is sent.
|
||||||
|
MaxAllocPeerRequestDataPerConn int64
|
||||||
|
|
||||||
|
// The IP addresses as our peers should see them. May differ from the
|
||||||
|
// local interfaces due to NAT or other network configurations.
|
||||||
|
PublicIp4 net.IP
|
||||||
|
PublicIp6 net.IP
|
||||||
|
|
||||||
|
// Accept rate limiting affects excessive connection attempts from IPs that fail during
|
||||||
|
// handshakes or request torrents that we don't have.
|
||||||
|
DisableAcceptRateLimiting bool
|
||||||
|
// Don't add connections that have the same peer ID as an existing
|
||||||
|
// connection for a given Torrent.
|
||||||
|
DropDuplicatePeerIds bool
|
||||||
|
// Drop peers that are complete if we are also complete and have no use for the peer. This is a
|
||||||
|
// bit of a special case, since a peer could also be useless if they're just not interested, or
|
||||||
|
// we don't intend to obtain all of a torrent's data.
|
||||||
|
DropMutuallyCompletePeers bool
|
||||||
|
// Whether to accept peer connections at all.
|
||||||
|
AcceptPeerConnections bool
|
||||||
|
// Whether a Client should want conns without delegating to any attached Torrents. This is
|
||||||
|
// useful when torrents might be added dynamically in callbacks for example.
|
||||||
|
AlwaysWantConns bool
|
||||||
|
|
||||||
|
Extensions PeerExtensionBits
|
||||||
|
// Bits that peers must have set to proceed past handshakes.
|
||||||
|
MinPeerExtensions PeerExtensionBits
|
||||||
|
|
||||||
|
DisableWebtorrent bool
|
||||||
|
DisableWebseeds bool
|
||||||
|
|
||||||
|
Callbacks Callbacks
|
||||||
|
|
||||||
|
// ICEServers defines a slice describing servers available to be used by
|
||||||
|
// ICE, such as STUN and TURN servers.
|
||||||
|
ICEServers []string
|
||||||
|
|
||||||
|
DialRateLimiter *rate.Limiter
|
||||||
|
|
||||||
|
PieceHashersPerTorrent int // default: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *ClientConfig) SetListenAddr(addr string) *ClientConfig {
|
||||||
|
host, port, err := missinggo.ParseHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
cfg.ListenHost = func(string) string { return host }
|
||||||
|
cfg.ListenPort = port
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultClientConfig() *ClientConfig {
|
||||||
|
cc := &ClientConfig{
|
||||||
|
HTTPUserAgent: version.DefaultHttpUserAgent,
|
||||||
|
ExtendedHandshakeClientVersion: version.DefaultExtendedHandshakeClientVersion,
|
||||||
|
Bep20: version.DefaultBep20Prefix,
|
||||||
|
UpnpID: version.DefaultUpnpId,
|
||||||
|
NominalDialTimeout: 20 * time.Second,
|
||||||
|
MinDialTimeout: 3 * time.Second,
|
||||||
|
EstablishedConnsPerTorrent: 50,
|
||||||
|
HalfOpenConnsPerTorrent: 25,
|
||||||
|
TotalHalfOpenConns: 100,
|
||||||
|
TorrentPeersHighWater: 500,
|
||||||
|
TorrentPeersLowWater: 50,
|
||||||
|
HandshakesTimeout: 4 * time.Second,
|
||||||
|
KeepAliveTimeout: time.Minute,
|
||||||
|
MaxAllocPeerRequestDataPerConn: 1 << 20,
|
||||||
|
ListenHost: func(string) string { return "" },
|
||||||
|
UploadRateLimiter: unlimited,
|
||||||
|
DownloadRateLimiter: unlimited,
|
||||||
|
DisableAcceptRateLimiting: true,
|
||||||
|
DropMutuallyCompletePeers: true,
|
||||||
|
HeaderObfuscationPolicy: HeaderObfuscationPolicy{
|
||||||
|
Preferred: true,
|
||||||
|
RequirePreferred: false,
|
||||||
|
},
|
||||||
|
CryptoSelector: mse.DefaultCryptoSelector,
|
||||||
|
CryptoProvides: mse.AllSupportedCrypto,
|
||||||
|
ListenPort: 42069,
|
||||||
|
Extensions: defaultPeerExtensionBytes(),
|
||||||
|
AcceptPeerConnections: true,
|
||||||
|
MaxUnverifiedBytes: 64 << 20,
|
||||||
|
DialRateLimiter: rate.NewLimiter(10, 10),
|
||||||
|
PieceHashersPerTorrent: 2,
|
||||||
|
}
|
||||||
|
cc.DhtStartingNodes = func(network string) dht.StartingNodesGetter {
|
||||||
|
return func() ([]dht.Addr, error) { return dht.GlobalBootstrapAddrs(network) }
|
||||||
|
}
|
||||||
|
cc.PeriodicallyAnnounceTorrentsToDht = true
|
||||||
|
return cc
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderObfuscationPolicy struct {
|
||||||
|
RequirePreferred bool // Whether the value of Preferred is a strict requirement.
|
||||||
|
Preferred bool // Whether header obfuscation is preferred.
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
pp "github.com/anacrolix/torrent/peer_protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Various connection-level metrics. At the Torrent level these are aggregates. Chunks are messages
|
||||||
|
// with data payloads. Data is actual torrent content without any overhead. Useful is something we
|
||||||
|
// needed locally. Unwanted is something we didn't ask for (but may still be useful). Written is
|
||||||
|
// things sent to the peer, and Read is stuff received from them. Due to the implementation of
|
||||||
|
// Count, must be aligned on some platforms: See https://github.com/anacrolix/torrent/issues/262.
|
||||||
|
type ConnStats struct {
|
||||||
|
// Total bytes on the wire. Includes handshakes and encryption.
|
||||||
|
BytesWritten Count
|
||||||
|
BytesWrittenData Count
|
||||||
|
|
||||||
|
BytesRead Count
|
||||||
|
BytesReadData Count
|
||||||
|
BytesReadUsefulData Count
|
||||||
|
BytesReadUsefulIntendedData Count
|
||||||
|
|
||||||
|
ChunksWritten Count
|
||||||
|
|
||||||
|
ChunksRead Count
|
||||||
|
ChunksReadUseful Count
|
||||||
|
ChunksReadWasted Count
|
||||||
|
|
||||||
|
MetadataChunksRead Count
|
||||||
|
|
||||||
|
// Number of pieces data was written to, that subsequently passed verification.
|
||||||
|
PiecesDirtiedGood Count
|
||||||
|
// Number of pieces data was written to, that subsequently failed verification. Note that a
|
||||||
|
// connection may not have been the sole dirtier of a piece.
|
||||||
|
PiecesDirtiedBad Count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *ConnStats) Copy() (ret ConnStats) {
|
||||||
|
for i := 0; i < reflect.TypeOf(ConnStats{}).NumField(); i++ {
|
||||||
|
n := reflect.ValueOf(me).Elem().Field(i).Addr().Interface().(*Count).Int64()
|
||||||
|
reflect.ValueOf(&ret).Elem().Field(i).Addr().Interface().(*Count).Add(n)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type Count struct {
|
||||||
|
n int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ fmt.Stringer = (*Count)(nil)
|
||||||
|
|
||||||
|
func (me *Count) Add(n int64) {
|
||||||
|
atomic.AddInt64(&me.n, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *Count) Int64() int64 {
|
||||||
|
return atomic.LoadInt64(&me.n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *Count) String() string {
|
||||||
|
return fmt.Sprintf("%v", me.Int64())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *Count) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(me.n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ConnStats) wroteMsg(msg *pp.Message) {
|
||||||
|
// TODO: Track messages and not just chunks.
|
||||||
|
switch msg.Type {
|
||||||
|
case pp.Piece:
|
||||||
|
cs.ChunksWritten.Add(1)
|
||||||
|
cs.BytesWrittenData.Add(int64(len(msg.Piece)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ConnStats) receivedChunk(size int64) {
|
||||||
|
cs.ChunksRead.Add(1)
|
||||||
|
cs.BytesReadData.Add(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ConnStats) incrementPiecesDirtiedGood() {
|
||||||
|
cs.PiecesDirtiedGood.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *ConnStats) incrementPiecesDirtiedBad() {
|
||||||
|
cs.PiecesDirtiedBad.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(n int64, f func(*ConnStats) *Count) func(*ConnStats) {
|
||||||
|
return func(cs *ConnStats) {
|
||||||
|
p := f(cs)
|
||||||
|
p.Add(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type connStatsReadWriter struct {
|
||||||
|
rw io.ReadWriter
|
||||||
|
c *PeerConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me connStatsReadWriter) Write(b []byte) (n int, err error) {
|
||||||
|
n, err = me.rw.Write(b)
|
||||||
|
me.c.wroteBytes(int64(n))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me connStatsReadWriter) Read(b []byte) (n int, err error) {
|
||||||
|
n, err = me.rw.Read(b)
|
||||||
|
me.c.readBytes(int64(n))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import "github.com/anacrolix/sync"
|
||||||
|
|
||||||
|
// Runs deferred actions on Unlock. Note that actions are assumed to be the results of changes that
|
||||||
|
// would only occur with a write lock at present. The race detector should catch instances of defers
|
||||||
|
// without the write lock being held.
|
||||||
|
type lockWithDeferreds struct {
|
||||||
|
internal sync.RWMutex
|
||||||
|
unlockActions []func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *lockWithDeferreds) Lock() {
|
||||||
|
me.internal.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *lockWithDeferreds) Unlock() {
|
||||||
|
unlockActions := me.unlockActions
|
||||||
|
for i := 0; i < len(unlockActions); i += 1 {
|
||||||
|
unlockActions[i]()
|
||||||
|
}
|
||||||
|
me.unlockActions = unlockActions[:0]
|
||||||
|
me.internal.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *lockWithDeferreds) RLock() {
|
||||||
|
me.internal.RLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *lockWithDeferreds) RUnlock() {
|
||||||
|
me.internal.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *lockWithDeferreds) Defer(action func()) {
|
||||||
|
me.unlockActions = append(me.unlockActions, action)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/anacrolix/dht/v2"
|
||||||
|
"github.com/anacrolix/dht/v2/krpc"
|
||||||
|
peer_store "github.com/anacrolix/dht/v2/peer-store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DHT server interface for use by a Torrent or Client. It's reasonable for this to make assumptions
|
||||||
|
// for torrent-use that might not be the default behaviour for the DHT server.
|
||||||
|
type DhtServer interface {
|
||||||
|
Stats() interface{}
|
||||||
|
ID() [20]byte
|
||||||
|
Addr() net.Addr
|
||||||
|
AddNode(ni krpc.NodeInfo) error
|
||||||
|
// This is called asynchronously when receiving PORT messages.
|
||||||
|
Ping(addr *net.UDPAddr)
|
||||||
|
Announce(hash [20]byte, port int, impliedPort bool) (DhtAnnounce, error)
|
||||||
|
WriteStatus(io.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional interface for DhtServer's that can expose their peer store (if any).
|
||||||
|
type PeerStorer interface {
|
||||||
|
PeerStore() peer_store.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
type DhtAnnounce interface {
|
||||||
|
Close()
|
||||||
|
Peers() <-chan dht.PeersValues
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnacrolixDhtServerWrapper struct {
|
||||||
|
*dht.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me AnacrolixDhtServerWrapper) Stats() interface{} {
|
||||||
|
return me.Server.Stats()
|
||||||
|
}
|
||||||
|
|
||||||
|
type anacrolixDhtAnnounceWrapper struct {
|
||||||
|
*dht.Announce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me anacrolixDhtAnnounceWrapper) Peers() <-chan dht.PeersValues {
|
||||||
|
return me.Announce.Peers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me AnacrolixDhtServerWrapper) Announce(hash [20]byte, port int, impliedPort bool) (DhtAnnounce, error) {
|
||||||
|
ann, err := me.Server.Announce(hash, port, impliedPort)
|
||||||
|
return anacrolixDhtAnnounceWrapper{ann}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me AnacrolixDhtServerWrapper) Ping(addr *net.UDPAddr) {
|
||||||
|
me.Server.PingQueryInput(addr, dht.QueryInput{
|
||||||
|
RateLimiting: dht.QueryRateLimiting{NoWaitFirst: true},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ DhtServer = AnacrolixDhtServerWrapper{}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dialPool struct {
|
||||||
|
resCh chan DialResult
|
||||||
|
addr string
|
||||||
|
left int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *dialPool) getFirst() (res DialResult) {
|
||||||
|
for me.left > 0 && res.Conn == nil {
|
||||||
|
res = <-me.resCh
|
||||||
|
me.left--
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *dialPool) add(ctx context.Context, dialer Dialer) {
|
||||||
|
me.left++
|
||||||
|
go func() {
|
||||||
|
me.resCh <- DialResult{
|
||||||
|
dialFromSocket(ctx, dialer, me.addr),
|
||||||
|
dialer,
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *dialPool) startDrainer() {
|
||||||
|
go me.drainAndCloseRemainingDials()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *dialPool) drainAndCloseRemainingDials() {
|
||||||
|
for me.left > 0 {
|
||||||
|
conn := (<-me.resCh).Conn
|
||||||
|
me.left--
|
||||||
|
if conn != nil {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anacrolix/torrent/dialer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Dialer = dialer.T
|
||||||
|
NetworkDialer = dialer.WithNetwork
|
||||||
|
)
|
||||||
|
|
||||||
|
var DefaultNetDialer = &dialer.Default
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dialers have the network locked in.
|
||||||
|
type T interface {
|
||||||
|
Dial(_ context.Context, addr string) (net.Conn, error)
|
||||||
|
DialerNetwork() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// An interface to ease wrapping dialers that explicitly include a network parameter.
|
||||||
|
type WithContext interface {
|
||||||
|
DialContext(ctx context.Context, network, addr string) (net.Conn, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used by wrappers of standard library network types.
|
||||||
|
var Default = &net.Dialer{}
|
||||||
|
|
||||||
|
// Adapts a WithContext to the Dial interface in this package.
|
||||||
|
type WithNetwork struct {
|
||||||
|
Network string
|
||||||
|
Dialer WithContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me WithNetwork) DialerNetwork() string {
|
||||||
|
return me.Network
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me WithNetwork) Dial(ctx context.Context, addr string) (_ net.Conn, err error) {
|
||||||
|
return me.Dialer.DialContext(ctx, me.Network, addr)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
Package torrent implements a torrent client. Goals include:
|
||||||
|
- Configurable data storage, such as file, mmap, and piece-based.
|
||||||
|
- Downloading on demand: torrent.Reader will request only the data required to
|
||||||
|
satisfy Reads, which is ideal for streaming and torrentfs.
|
||||||
|
|
||||||
|
BitTorrent features implemented include:
|
||||||
|
- Protocol obfuscation
|
||||||
|
- DHT
|
||||||
|
- uTP
|
||||||
|
- PEX
|
||||||
|
- Magnet links
|
||||||
|
- IP Blocklists
|
||||||
|
- Some IPv6
|
||||||
|
- HTTP and UDP tracker clients
|
||||||
|
- BEPs:
|
||||||
|
- 3: Basic BitTorrent protocol
|
||||||
|
- 5: DHT
|
||||||
|
- 6: Fast Extension (have all/none only)
|
||||||
|
- 7: IPv6 Tracker Extension
|
||||||
|
- 9: ut_metadata
|
||||||
|
- 10: Extension protocol
|
||||||
|
- 11: PEX
|
||||||
|
- 12: Multitracker metadata extension
|
||||||
|
- 15: UDP Tracker Protocol
|
||||||
|
- 20: Peer ID convention ("-GTnnnn-")
|
||||||
|
- 23: Tracker Returns Compact Peer Lists
|
||||||
|
- 29: uTorrent transport protocol
|
||||||
|
- 41: UDP Tracker Protocol Extensions
|
||||||
|
- 42: DHT Security extension
|
||||||
|
- 43: Read-only DHT Nodes
|
||||||
|
*/
|
||||||
|
package torrent
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package torrent_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example() {
|
||||||
|
c, _ := torrent.NewClient(nil)
|
||||||
|
defer c.Close()
|
||||||
|
t, _ := c.AddMagnet("magnet:?xt=urn:btih:ZOCMZQIPFFW7OLLMIC5HUB6BPCSDEOQU")
|
||||||
|
<-t.GotInfo()
|
||||||
|
t.DownloadAll()
|
||||||
|
c.WaitAll()
|
||||||
|
log.Print("ermahgerd, torrent downloaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example_fileReader() {
|
||||||
|
var f torrent.File
|
||||||
|
// Accesses the parts of the torrent pertaining to f. Data will be
|
||||||
|
// downloaded as required, per the configuration of the torrent.Reader.
|
||||||
|
r := f.NewReader()
|
||||||
|
defer r.Close()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/RoaringBitmap/roaring"
|
||||||
|
"github.com/anacrolix/missinggo/v2/bitmap"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provides access to regions of torrent data that correspond to its files.
|
||||||
|
type File struct {
|
||||||
|
t *Torrent
|
||||||
|
path string
|
||||||
|
offset int64
|
||||||
|
length int64
|
||||||
|
fi metainfo.FileInfo
|
||||||
|
displayPath string
|
||||||
|
prio piecePriority
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Torrent() *Torrent {
|
||||||
|
return f.t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data for this file begins this many bytes into the Torrent.
|
||||||
|
func (f *File) Offset() int64 {
|
||||||
|
return f.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
// The FileInfo from the metainfo.Info to which this file corresponds.
|
||||||
|
func (f File) FileInfo() metainfo.FileInfo {
|
||||||
|
return f.fi
|
||||||
|
}
|
||||||
|
|
||||||
|
// The file's path components joined by '/'.
|
||||||
|
func (f File) Path() string {
|
||||||
|
return f.path
|
||||||
|
}
|
||||||
|
|
||||||
|
// The file's length in bytes.
|
||||||
|
func (f *File) Length() int64 {
|
||||||
|
return f.length
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number of bytes of the entire file we have completed. This is the sum of
|
||||||
|
// completed pieces, and dirtied chunks of incomplete pieces.
|
||||||
|
func (f *File) BytesCompleted() (n int64) {
|
||||||
|
f.t.cl.rLock()
|
||||||
|
n = f.bytesCompletedLocked()
|
||||||
|
f.t.cl.rUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) bytesCompletedLocked() int64 {
|
||||||
|
return f.length - f.bytesLeft()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileBytesLeft(
|
||||||
|
torrentUsualPieceSize int64,
|
||||||
|
fileFirstPieceIndex int,
|
||||||
|
fileEndPieceIndex int,
|
||||||
|
fileTorrentOffset int64,
|
||||||
|
fileLength int64,
|
||||||
|
torrentCompletedPieces *roaring.Bitmap,
|
||||||
|
pieceSizeCompletedFn func(pieceIndex int) int64,
|
||||||
|
) (left int64) {
|
||||||
|
if fileLength == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
noCompletedMiddlePieces := roaring.New()
|
||||||
|
noCompletedMiddlePieces.AddRange(bitmap.BitRange(fileFirstPieceIndex), bitmap.BitRange(fileEndPieceIndex))
|
||||||
|
noCompletedMiddlePieces.AndNot(torrentCompletedPieces)
|
||||||
|
noCompletedMiddlePieces.Iterate(func(pieceIndex uint32) bool {
|
||||||
|
i := int(pieceIndex)
|
||||||
|
pieceSizeCompleted := pieceSizeCompletedFn(i)
|
||||||
|
if i == fileFirstPieceIndex {
|
||||||
|
beginOffset := fileTorrentOffset % torrentUsualPieceSize
|
||||||
|
beginSize := torrentUsualPieceSize - beginOffset
|
||||||
|
beginDownLoaded := pieceSizeCompleted - beginOffset
|
||||||
|
if beginDownLoaded < 0 {
|
||||||
|
beginDownLoaded = 0
|
||||||
|
}
|
||||||
|
left += beginSize - beginDownLoaded
|
||||||
|
} else if i == fileEndPieceIndex-1 {
|
||||||
|
endSize := (fileTorrentOffset + fileLength) % torrentUsualPieceSize
|
||||||
|
if endSize == 0 {
|
||||||
|
endSize = torrentUsualPieceSize
|
||||||
|
}
|
||||||
|
endDownloaded := pieceSizeCompleted
|
||||||
|
if endDownloaded > endSize {
|
||||||
|
endDownloaded = endSize
|
||||||
|
}
|
||||||
|
left += endSize - endDownloaded
|
||||||
|
} else {
|
||||||
|
left += torrentUsualPieceSize - pieceSizeCompleted
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if left > fileLength {
|
||||||
|
left = fileLength
|
||||||
|
}
|
||||||
|
//
|
||||||
|
//numPiecesSpanned := f.EndPieceIndex() - f.BeginPieceIndex()
|
||||||
|
//completedMiddlePieces := f.t._completedPieces.Clone()
|
||||||
|
//completedMiddlePieces.RemoveRange(0, bitmap.BitRange(f.BeginPieceIndex()+1))
|
||||||
|
//completedMiddlePieces.RemoveRange(bitmap.BitRange(f.EndPieceIndex()-1), bitmap.ToEnd)
|
||||||
|
//left += int64(numPiecesSpanned-2-pieceIndex(completedMiddlePieces.GetCardinality())) * torrentUsualPieceSize
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) bytesLeft() (left int64) {
|
||||||
|
return fileBytesLeft(int64(f.t.usualPieceSize()), f.BeginPieceIndex(), f.EndPieceIndex(), f.offset, f.length, &f.t._completedPieces, func(pieceIndex int) int64 {
|
||||||
|
return int64(f.t.piece(pieceIndex).numDirtyBytes())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// The relative file path for a multi-file torrent, and the torrent name for a
|
||||||
|
// single-file torrent. Dir separators are '/'.
|
||||||
|
func (f *File) DisplayPath() string {
|
||||||
|
return f.displayPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// The download status of a piece that comprises part of a File.
|
||||||
|
type FilePieceState struct {
|
||||||
|
Bytes int64 // Bytes within the piece that are part of this File.
|
||||||
|
PieceState
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the state of pieces in this file.
|
||||||
|
func (f *File) State() (ret []FilePieceState) {
|
||||||
|
f.t.cl.rLock()
|
||||||
|
defer f.t.cl.rUnlock()
|
||||||
|
pieceSize := int64(f.t.usualPieceSize())
|
||||||
|
off := f.offset % pieceSize
|
||||||
|
remaining := f.length
|
||||||
|
for i := pieceIndex(f.offset / pieceSize); ; i++ {
|
||||||
|
if remaining == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
len1 := pieceSize - off
|
||||||
|
if len1 > remaining {
|
||||||
|
len1 = remaining
|
||||||
|
}
|
||||||
|
ps := f.t.pieceState(i)
|
||||||
|
ret = append(ret, FilePieceState{len1, ps})
|
||||||
|
off = 0
|
||||||
|
remaining -= len1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requests that all pieces containing data in the file be downloaded.
|
||||||
|
func (f *File) Download() {
|
||||||
|
f.SetPriority(PiecePriorityNormal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func byteRegionExclusivePieces(off, size, pieceSize int64) (begin, end int) {
|
||||||
|
begin = int((off + pieceSize - 1) / pieceSize)
|
||||||
|
end = int((off + size) / pieceSize)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use File.SetPriority.
|
||||||
|
func (f *File) Cancel() {
|
||||||
|
f.SetPriority(PiecePriorityNone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) NewReader() Reader {
|
||||||
|
return f.t.newReader(f.Offset(), f.Length())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the minimum priority for pieces in the File.
|
||||||
|
func (f *File) SetPriority(prio piecePriority) {
|
||||||
|
f.t.cl.lock()
|
||||||
|
if prio != f.prio {
|
||||||
|
f.prio = prio
|
||||||
|
f.t.updatePiecePriorities(f.BeginPieceIndex(), f.EndPieceIndex(), "File.SetPriority")
|
||||||
|
}
|
||||||
|
f.t.cl.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the priority per File.SetPriority.
|
||||||
|
func (f *File) Priority() (prio piecePriority) {
|
||||||
|
f.t.cl.rLock()
|
||||||
|
prio = f.prio
|
||||||
|
f.t.cl.rUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the index of the first piece containing data for the file.
|
||||||
|
func (f *File) BeginPieceIndex() int {
|
||||||
|
if f.t.usualPieceSize() == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return pieceIndex(f.offset / int64(f.t.usualPieceSize()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the index of the piece after the last one containing data for the file.
|
||||||
|
func (f *File) EndPieceIndex() int {
|
||||||
|
if f.t.usualPieceSize() == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return pieceIndex((f.offset + f.length + int64(f.t.usualPieceSize()) - 1) / int64(f.t.usualPieceSize()))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/RoaringBitmap/roaring"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileExclusivePieces(t *testing.T) {
|
||||||
|
for _, _case := range []struct {
|
||||||
|
off, size, pieceSize int64
|
||||||
|
begin, end int
|
||||||
|
}{
|
||||||
|
{0, 2, 2, 0, 1},
|
||||||
|
{1, 2, 2, 1, 1},
|
||||||
|
{1, 4, 2, 1, 2},
|
||||||
|
} {
|
||||||
|
begin, end := byteRegionExclusivePieces(_case.off, _case.size, _case.pieceSize)
|
||||||
|
assert.EqualValues(t, _case.begin, begin)
|
||||||
|
assert.EqualValues(t, _case.end, end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testFileBytesLeft struct {
|
||||||
|
usualPieceSize int64
|
||||||
|
firstPieceIndex int
|
||||||
|
endPieceIndex int
|
||||||
|
fileOffset int64
|
||||||
|
fileLength int64
|
||||||
|
completedPieces roaring.Bitmap
|
||||||
|
expected int64
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me testFileBytesLeft) Run(t *testing.T) {
|
||||||
|
t.Run(me.name, func(t *testing.T) {
|
||||||
|
assert.EqualValues(t, me.expected, fileBytesLeft(me.usualPieceSize, me.firstPieceIndex, me.endPieceIndex, me.fileOffset, me.fileLength, &me.completedPieces, func(pieceIndex int) int64 {
|
||||||
|
return 0
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileBytesLeft(t *testing.T) {
|
||||||
|
testFileBytesLeft{
|
||||||
|
usualPieceSize: 3,
|
||||||
|
firstPieceIndex: 1,
|
||||||
|
endPieceIndex: 1,
|
||||||
|
fileOffset: 1,
|
||||||
|
fileLength: 0,
|
||||||
|
expected: 0,
|
||||||
|
name: "ZeroLengthFile",
|
||||||
|
}.Run(t)
|
||||||
|
|
||||||
|
testFileBytesLeft{
|
||||||
|
usualPieceSize: 2,
|
||||||
|
firstPieceIndex: 1,
|
||||||
|
endPieceIndex: 2,
|
||||||
|
fileOffset: 1,
|
||||||
|
fileLength: 1,
|
||||||
|
expected: 1,
|
||||||
|
name: "EndOfSecondPiece",
|
||||||
|
}.Run(t)
|
||||||
|
|
||||||
|
testFileBytesLeft{
|
||||||
|
usualPieceSize: 3,
|
||||||
|
firstPieceIndex: 0,
|
||||||
|
endPieceIndex: 1,
|
||||||
|
fileOffset: 1,
|
||||||
|
fileLength: 1,
|
||||||
|
expected: 1,
|
||||||
|
name: "FileInFirstPiece",
|
||||||
|
}.Run(t)
|
||||||
|
|
||||||
|
testFileBytesLeft{
|
||||||
|
usualPieceSize: 3,
|
||||||
|
firstPieceIndex: 0,
|
||||||
|
endPieceIndex: 1,
|
||||||
|
fileOffset: 1,
|
||||||
|
fileLength: 1,
|
||||||
|
expected: 1,
|
||||||
|
name: "LandLocked",
|
||||||
|
}.Run(t)
|
||||||
|
|
||||||
|
testFileBytesLeft{
|
||||||
|
usualPieceSize: 3,
|
||||||
|
firstPieceIndex: 1,
|
||||||
|
endPieceIndex: 3,
|
||||||
|
fileOffset: 4,
|
||||||
|
fileLength: 4,
|
||||||
|
expected: 4,
|
||||||
|
name: "TwoPieces",
|
||||||
|
}.Run(t)
|
||||||
|
|
||||||
|
testFileBytesLeft{
|
||||||
|
usualPieceSize: 3,
|
||||||
|
firstPieceIndex: 1,
|
||||||
|
endPieceIndex: 4,
|
||||||
|
fileOffset: 5,
|
||||||
|
fileLength: 7,
|
||||||
|
expected: 7,
|
||||||
|
name: "ThreePieces",
|
||||||
|
}.Run(t)
|
||||||
|
|
||||||
|
testFileBytesLeft{
|
||||||
|
usualPieceSize: 3,
|
||||||
|
firstPieceIndex: 1,
|
||||||
|
endPieceIndex: 4,
|
||||||
|
fileOffset: 5,
|
||||||
|
fileLength: 7,
|
||||||
|
expected: 0,
|
||||||
|
completedPieces: func() (ret roaring.Bitmap) {
|
||||||
|
ret.AddRange(0, 5)
|
||||||
|
return
|
||||||
|
}(),
|
||||||
|
name: "ThreePiecesCompletedAll",
|
||||||
|
}.Run(t)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
* Reinstate InitAsyncRead, or find out if it's worth it. Upstream made it a PITA to apply it automatically.
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
// Mounts a FUSE filesystem backed by torrents and magnet links.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
_ "net/http/pprof"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anacrolix/envpprof"
|
||||||
|
_ "github.com/anacrolix/envpprof"
|
||||||
|
"github.com/anacrolix/fuse"
|
||||||
|
fusefs "github.com/anacrolix/fuse/fs"
|
||||||
|
"github.com/anacrolix/log"
|
||||||
|
"github.com/anacrolix/tagflag"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
torrentfs "github.com/anacrolix/torrent/fs"
|
||||||
|
"github.com/anacrolix/torrent/util/dirwatch"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger = log.Default.WithNames("main")
|
||||||
|
|
||||||
|
var args = struct {
|
||||||
|
MetainfoDir string `help:"torrent files in this location describe the contents of the mounted filesystem"`
|
||||||
|
DownloadDir string `help:"location to save torrent data"`
|
||||||
|
MountDir string `help:"location the torrent contents are made available"`
|
||||||
|
|
||||||
|
DisableTrackers bool
|
||||||
|
TestPeer *net.TCPAddr
|
||||||
|
ReadaheadBytes tagflag.Bytes
|
||||||
|
ListenAddr *net.TCPAddr
|
||||||
|
}{
|
||||||
|
MetainfoDir: func() string {
|
||||||
|
_user, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return filepath.Join(_user.HomeDir, ".config/transmission/torrents")
|
||||||
|
}(),
|
||||||
|
ReadaheadBytes: 10 << 20,
|
||||||
|
ListenAddr: &net.TCPAddr{},
|
||||||
|
}
|
||||||
|
|
||||||
|
func exitSignalHandlers(fs *torrentfs.TorrentFS) {
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
for {
|
||||||
|
<-c
|
||||||
|
fs.Destroy()
|
||||||
|
err := fuse.Unmount(args.MountDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addTestPeer(client *torrent.Client) {
|
||||||
|
for _, t := range client.Torrents() {
|
||||||
|
t.AddPeers([]torrent.PeerInfo{{
|
||||||
|
Addr: args.TestPeer,
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
defer envpprof.Stop()
|
||||||
|
err := mainErr()
|
||||||
|
if err != nil {
|
||||||
|
logger.Levelf(log.Error, "error in main: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mainErr() error {
|
||||||
|
tagflag.Parse(&args)
|
||||||
|
if args.MountDir == "" {
|
||||||
|
os.Stderr.WriteString("y u no specify mountpoint?\n")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
conn, err := fuse.Mount(args.MountDir, fuse.ReadOnly())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("mounting: %w", err)
|
||||||
|
}
|
||||||
|
defer fuse.Unmount(args.MountDir)
|
||||||
|
// TODO: Think about the ramifications of exiting not due to a signal.
|
||||||
|
defer conn.Close()
|
||||||
|
cfg := torrent.NewDefaultClientConfig()
|
||||||
|
cfg.DataDir = args.DownloadDir
|
||||||
|
cfg.DisableTrackers = args.DisableTrackers
|
||||||
|
cfg.NoUpload = true // Ensure that downloads are responsive.
|
||||||
|
cfg.SetListenAddr(args.ListenAddr.String())
|
||||||
|
client, err := torrent.NewClient(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating torrent client: %w", err)
|
||||||
|
}
|
||||||
|
// This is naturally exported via GOPPROF=http.
|
||||||
|
http.DefaultServeMux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
client.WriteStatus(w)
|
||||||
|
})
|
||||||
|
dw, err := dirwatch.New(args.MetainfoDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("watching torrent dir: %w", err)
|
||||||
|
}
|
||||||
|
dw.Logger = dw.Logger.FilterLevel(log.Info)
|
||||||
|
go func() {
|
||||||
|
for ev := range dw.Events {
|
||||||
|
switch ev.Change {
|
||||||
|
case dirwatch.Added:
|
||||||
|
if ev.TorrentFilePath != "" {
|
||||||
|
_, err := client.AddTorrentFromFile(ev.TorrentFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error adding torrent from file %q to client: %v", ev.TorrentFilePath, err)
|
||||||
|
}
|
||||||
|
} else if ev.MagnetURI != "" {
|
||||||
|
_, err := client.AddMagnet(ev.MagnetURI)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error adding magnet: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case dirwatch.Removed:
|
||||||
|
T, ok := client.Torrent(ev.InfoHash)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
T.Drop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
fs := torrentfs.New(client)
|
||||||
|
go exitSignalHandlers(fs)
|
||||||
|
|
||||||
|
if args.TestPeer != nil {
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
addTestPeer(client)
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Levelf(log.Debug, "serving fuse fs")
|
||||||
|
if err := fusefs.Serve(conn, fs); err != nil {
|
||||||
|
return fmt.Errorf("serving fuse fs: %w", err)
|
||||||
|
}
|
||||||
|
logger.Levelf(log.Debug, "fuse fs completed successfully. waiting for conn ready")
|
||||||
|
<-conn.Ready
|
||||||
|
if err := conn.MountError; err != nil {
|
||||||
|
return fmt.Errorf("mount error: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
package torrentfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/anacrolix/fuse"
|
||||||
|
"github.com/anacrolix/fuse/fs"
|
||||||
|
"github.com/anacrolix/missinggo/v2"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileHandle struct {
|
||||||
|
fn fileNode
|
||||||
|
tf *torrent.File
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ interface {
|
||||||
|
fs.HandleReader
|
||||||
|
fs.HandleReleaser
|
||||||
|
} = fileHandle{}
|
||||||
|
|
||||||
|
func (me fileHandle) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error {
|
||||||
|
torrentfsReadRequests.Add(1)
|
||||||
|
if req.Dir {
|
||||||
|
panic("read on directory")
|
||||||
|
}
|
||||||
|
r := me.tf.NewReader()
|
||||||
|
defer r.Close()
|
||||||
|
pos, err := r.Seek(req.Offset, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if pos != req.Offset {
|
||||||
|
panic("seek failed")
|
||||||
|
}
|
||||||
|
resp.Data = resp.Data[:req.Size]
|
||||||
|
readDone := make(chan struct{})
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
var readErr error
|
||||||
|
go func() {
|
||||||
|
defer close(readDone)
|
||||||
|
me.fn.FS.mu.Lock()
|
||||||
|
me.fn.FS.blockedReads++
|
||||||
|
me.fn.FS.event.Broadcast()
|
||||||
|
me.fn.FS.mu.Unlock()
|
||||||
|
var n int
|
||||||
|
r := missinggo.ContextedReader{r, ctx}
|
||||||
|
// log.Printf("reading %v bytes at %v", len(resp.Data), req.Offset)
|
||||||
|
if true {
|
||||||
|
// A user reported on that on freebsd 12.2, the system requires that reads are
|
||||||
|
// completely filled. Their system only asks for 64KiB at a time. I've seen systems that
|
||||||
|
// can demand up to 16MiB at a time, so this gets tricky. For now, I'll restore the old
|
||||||
|
// behaviour from before 2a7352a, which nobody reported problems with.
|
||||||
|
n, readErr = io.ReadFull(r, resp.Data)
|
||||||
|
if readErr == io.ErrUnexpectedEOF {
|
||||||
|
readErr = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
n, readErr = r.Read(resp.Data)
|
||||||
|
if readErr == io.EOF {
|
||||||
|
readErr = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.Data = resp.Data[:n]
|
||||||
|
}()
|
||||||
|
defer func() {
|
||||||
|
<-readDone
|
||||||
|
me.fn.FS.mu.Lock()
|
||||||
|
me.fn.FS.blockedReads--
|
||||||
|
me.fn.FS.event.Broadcast()
|
||||||
|
me.fn.FS.mu.Unlock()
|
||||||
|
}()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-readDone:
|
||||||
|
return readErr
|
||||||
|
case <-me.fn.FS.destroyed:
|
||||||
|
return fuse.EIO
|
||||||
|
case <-ctx.Done():
|
||||||
|
return fuse.EINTR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me fileHandle) Release(context.Context, *fuse.ReleaseRequest) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package torrentfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/anacrolix/fuse"
|
||||||
|
fusefs "github.com/anacrolix/fuse/fs"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileNode struct {
|
||||||
|
node
|
||||||
|
f *torrent.File
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ fusefs.NodeOpener = fileNode{}
|
||||||
|
|
||||||
|
func (fn fileNode) Attr(ctx context.Context, attr *fuse.Attr) error {
|
||||||
|
attr.Size = uint64(fn.f.Length())
|
||||||
|
attr.Mode = defaultMode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fn fileNode) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fusefs.Handle, error) {
|
||||||
|
return fileHandle{fn, fn.f}, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo $BASH_VERSION
|
||||||
|
set -eux
|
||||||
|
repopath="$(cd "$(dirname "$0")/.."; pwd)"
|
||||||
|
debian_file=debian-10.8.0-amd64-netinst.iso
|
||||||
|
mkdir -p mnt torrents
|
||||||
|
# I think the timing can cause torrents to not get added correctly to the torrentfs client, so add
|
||||||
|
# them first and start the fs afterwards.
|
||||||
|
pushd torrents
|
||||||
|
cp "$repopath/testdata/$debian_file.torrent" .
|
||||||
|
godo -v -- "$repopath/cmd/torrent" metainfo "$repopath/testdata/sintel.torrent" magnet > sintel.magnet
|
||||||
|
popd
|
||||||
|
#file="$debian_file"
|
||||||
|
file=Sintel/Sintel.mp4
|
||||||
|
|
||||||
|
GOPPROF=http godo -v -- "$repopath/fs/cmd/torrentfs" -mountDir=mnt -metainfoDir=torrents &
|
||||||
|
torrentfs_pid=$!
|
||||||
|
trap "kill $torrentfs_pid" EXIT
|
||||||
|
|
||||||
|
check_file() {
|
||||||
|
while [ ! -e "mnt/$file" ]; do sleep 1; done
|
||||||
|
pv -f "mnt/$file" | md5sum -c <(cat <<-EOF
|
||||||
|
083e808d56aa7b146f513b3458658292 -
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
( check_file ) &
|
||||||
|
check_file_pid=$!
|
||||||
|
|
||||||
|
trap "kill $torrentfs_pid $check_file_pid" EXIT
|
||||||
|
wait -n
|
||||||
|
status=$?
|
||||||
|
sudo umount mnt
|
||||||
|
trap - EXIT
|
||||||
|
echo "wait returned" $status
|
||||||
|
exit $status
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
package torrentfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"expvar"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/anacrolix/fuse"
|
||||||
|
fusefs "github.com/anacrolix/fuse/fs"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultMode = 0o555
|
||||||
|
)
|
||||||
|
|
||||||
|
var torrentfsReadRequests = expvar.NewInt("torrentfsReadRequests")
|
||||||
|
|
||||||
|
type TorrentFS struct {
|
||||||
|
Client *torrent.Client
|
||||||
|
destroyed chan struct{}
|
||||||
|
mu sync.Mutex
|
||||||
|
blockedReads int
|
||||||
|
event sync.Cond
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ fusefs.FSDestroyer = &TorrentFS{}
|
||||||
|
|
||||||
|
_ fusefs.NodeForgetter = rootNode{}
|
||||||
|
_ fusefs.HandleReadDirAller = rootNode{}
|
||||||
|
_ fusefs.HandleReadDirAller = dirNode{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Is a directory node that lists all torrents and handles destruction of the
|
||||||
|
// filesystem.
|
||||||
|
type rootNode struct {
|
||||||
|
fs *TorrentFS
|
||||||
|
}
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
path string
|
||||||
|
metadata *metainfo.Info
|
||||||
|
FS *TorrentFS
|
||||||
|
t *torrent.Torrent
|
||||||
|
}
|
||||||
|
|
||||||
|
type dirNode struct {
|
||||||
|
node
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ fusefs.HandleReadDirAller = dirNode{}
|
||||||
|
|
||||||
|
func isSubPath(parent, child string) bool {
|
||||||
|
if parent == "" {
|
||||||
|
return len(child) > 0
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(child, parent) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
extra := child[len(parent):]
|
||||||
|
if extra == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Not just a file with more stuff on the end.
|
||||||
|
return extra[0] == '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dn dirNode) ReadDirAll(ctx context.Context) (des []fuse.Dirent, err error) {
|
||||||
|
names := map[string]bool{}
|
||||||
|
for _, fi := range dn.metadata.Files {
|
||||||
|
filePathname := strings.Join(fi.Path, "/")
|
||||||
|
if !isSubPath(dn.path, filePathname) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var name string
|
||||||
|
if dn.path == "" {
|
||||||
|
name = fi.Path[0]
|
||||||
|
} else {
|
||||||
|
dirPathname := strings.Split(dn.path, "/")
|
||||||
|
name = fi.Path[len(dirPathname)]
|
||||||
|
}
|
||||||
|
if names[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
names[name] = true
|
||||||
|
de := fuse.Dirent{
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
if len(fi.Path) == len(dn.path)+1 {
|
||||||
|
de.Type = fuse.DT_File
|
||||||
|
} else {
|
||||||
|
de.Type = fuse.DT_Dir
|
||||||
|
}
|
||||||
|
des = append(des, de)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dn dirNode) Lookup(_ context.Context, name string) (fusefs.Node, error) {
|
||||||
|
dir := false
|
||||||
|
var file *torrent.File
|
||||||
|
var fullPath string
|
||||||
|
if dn.path != "" {
|
||||||
|
fullPath = dn.path + "/" + name
|
||||||
|
} else {
|
||||||
|
fullPath = name
|
||||||
|
}
|
||||||
|
for _, f := range dn.t.Files() {
|
||||||
|
if f.DisplayPath() == fullPath {
|
||||||
|
file = f
|
||||||
|
}
|
||||||
|
if isSubPath(fullPath, f.DisplayPath()) {
|
||||||
|
dir = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n := dn.node
|
||||||
|
n.path = fullPath
|
||||||
|
if dir && file != nil {
|
||||||
|
panic("both dir and file")
|
||||||
|
}
|
||||||
|
if file != nil {
|
||||||
|
return fileNode{n, file}, nil
|
||||||
|
}
|
||||||
|
if dir {
|
||||||
|
return dirNode{n}, nil
|
||||||
|
}
|
||||||
|
return nil, fuse.ENOENT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dn dirNode) Attr(ctx context.Context, attr *fuse.Attr) error {
|
||||||
|
attr.Mode = os.ModeDir | defaultMode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rn rootNode) Lookup(ctx context.Context, name string) (_node fusefs.Node, err error) {
|
||||||
|
for _, t := range rn.fs.Client.Torrents() {
|
||||||
|
info := t.Info()
|
||||||
|
if t.Name() != name || info == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
__node := node{
|
||||||
|
metadata: info,
|
||||||
|
FS: rn.fs,
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
_node = fileNode{__node, t.Files()[0]}
|
||||||
|
} else {
|
||||||
|
_node = dirNode{__node}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _node == nil {
|
||||||
|
err = fuse.ENOENT
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rn rootNode) ReadDirAll(ctx context.Context) (dirents []fuse.Dirent, err error) {
|
||||||
|
for _, t := range rn.fs.Client.Torrents() {
|
||||||
|
info := t.Info()
|
||||||
|
if info == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dirents = append(dirents, fuse.Dirent{
|
||||||
|
Name: info.Name,
|
||||||
|
Type: func() fuse.DirentType {
|
||||||
|
if !info.IsDir() {
|
||||||
|
return fuse.DT_File
|
||||||
|
} else {
|
||||||
|
return fuse.DT_Dir
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rn rootNode) Attr(ctx context.Context, attr *fuse.Attr) error {
|
||||||
|
attr.Mode = os.ModeDir | defaultMode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(anacrolix): Why should rootNode implement this?
|
||||||
|
func (rn rootNode) Forget() {
|
||||||
|
rn.fs.Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfs *TorrentFS) Root() (fusefs.Node, error) {
|
||||||
|
return rootNode{tfs}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tfs *TorrentFS) Destroy() {
|
||||||
|
tfs.mu.Lock()
|
||||||
|
select {
|
||||||
|
case <-tfs.destroyed:
|
||||||
|
default:
|
||||||
|
close(tfs.destroyed)
|
||||||
|
}
|
||||||
|
tfs.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cl *torrent.Client) *TorrentFS {
|
||||||
|
fs := &TorrentFS{
|
||||||
|
Client: cl,
|
||||||
|
destroyed: make(chan struct{}),
|
||||||
|
}
|
||||||
|
fs.event.L = &fs.mu
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
package torrentfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
_ "net/http/pprof"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/anacrolix/envpprof"
|
||||||
|
"github.com/anacrolix/fuse"
|
||||||
|
fusefs "github.com/anacrolix/fuse/fs"
|
||||||
|
"github.com/anacrolix/missinggo/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/internal/testutil"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetFlags(log.Flags() | log.Lshortfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTCPAddrString(t *testing.T) {
|
||||||
|
l, err := net.Listen("tcp4", "localhost:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer l.Close()
|
||||||
|
c, err := net.Dial("tcp", l.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
ras := c.RemoteAddr().String()
|
||||||
|
ta := &net.TCPAddr{
|
||||||
|
IP: net.IPv4(127, 0, 0, 1),
|
||||||
|
Port: missinggo.AddrPort(l.Addr()),
|
||||||
|
}
|
||||||
|
s := ta.String()
|
||||||
|
if ras != s {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testLayout struct {
|
||||||
|
BaseDir string
|
||||||
|
MountDir string
|
||||||
|
Completed string
|
||||||
|
Metainfo *metainfo.MetaInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tl *testLayout) Destroy() error {
|
||||||
|
return os.RemoveAll(tl.BaseDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGreetingLayout(t *testing.T) (tl testLayout, err error) {
|
||||||
|
tl.BaseDir = t.TempDir()
|
||||||
|
tl.Completed = filepath.Join(tl.BaseDir, "completed")
|
||||||
|
os.Mkdir(tl.Completed, 0o777)
|
||||||
|
tl.MountDir = filepath.Join(tl.BaseDir, "mnt")
|
||||||
|
os.Mkdir(tl.MountDir, 0o777)
|
||||||
|
testutil.CreateDummyTorrentData(tl.Completed)
|
||||||
|
tl.Metainfo = testutil.GreetingMetaInfo()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmount without first killing the FUSE connection while there are FUSE
|
||||||
|
// operations blocked inside the filesystem code.
|
||||||
|
func TestUnmountWedged(t *testing.T) {
|
||||||
|
layout, err := newGreetingLayout(t)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() {
|
||||||
|
err := layout.Destroy()
|
||||||
|
if err != nil {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
cfg := torrent.NewDefaultClientConfig()
|
||||||
|
cfg.DataDir = filepath.Join(layout.BaseDir, "incomplete")
|
||||||
|
cfg.DisableTrackers = true
|
||||||
|
cfg.NoDHT = true
|
||||||
|
cfg.DisableTCP = true
|
||||||
|
cfg.DisableUTP = true
|
||||||
|
client, err := torrent.NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer client.Close()
|
||||||
|
tt, err := client.AddTorrent(layout.Metainfo)
|
||||||
|
require.NoError(t, err)
|
||||||
|
fs := New(client)
|
||||||
|
fuseConn, err := fuse.Mount(layout.MountDir)
|
||||||
|
if err != nil {
|
||||||
|
switch err.Error() {
|
||||||
|
case "cannot locate OSXFUSE":
|
||||||
|
fallthrough
|
||||||
|
case "fusermount: exit status 1":
|
||||||
|
t.Skip(err)
|
||||||
|
}
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
server := fusefs.New(fuseConn, &fusefs.Config{
|
||||||
|
Debug: func(msg interface{}) {
|
||||||
|
t.Log(msg)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
server.Serve(fs)
|
||||||
|
}()
|
||||||
|
<-fuseConn.Ready
|
||||||
|
if err := fuseConn.MountError; err != nil {
|
||||||
|
t.Fatalf("mount error: %s", err)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
// Read the greeting file, though it will never be available. This should
|
||||||
|
// "wedge" FUSE, requiring the fs object to be forcibly destroyed. The
|
||||||
|
// read call will return with a FS error.
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
fs.mu.Lock()
|
||||||
|
fs.event.Broadcast()
|
||||||
|
fs.mu.Unlock()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer cancel()
|
||||||
|
_, err := ioutil.ReadFile(filepath.Join(layout.MountDir, tt.Info().Name))
|
||||||
|
require.Error(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait until the read has blocked inside the filesystem code.
|
||||||
|
fs.mu.Lock()
|
||||||
|
for fs.blockedReads != 1 && ctx.Err() == nil {
|
||||||
|
fs.event.Wait()
|
||||||
|
}
|
||||||
|
fs.mu.Unlock()
|
||||||
|
|
||||||
|
fs.Destroy()
|
||||||
|
|
||||||
|
for {
|
||||||
|
err = fuse.Unmount(layout.MountDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("error unmounting: %s", err)
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fuseConn.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadOnDemand(t *testing.T) {
|
||||||
|
layout, err := newGreetingLayout(t)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer layout.Destroy()
|
||||||
|
cfg := torrent.NewDefaultClientConfig()
|
||||||
|
cfg.DataDir = layout.Completed
|
||||||
|
cfg.DisableTrackers = true
|
||||||
|
cfg.NoDHT = true
|
||||||
|
cfg.Seed = true
|
||||||
|
cfg.ListenPort = 0
|
||||||
|
cfg.ListenHost = torrent.LoopbackListenHost
|
||||||
|
seeder, err := torrent.NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer seeder.Close()
|
||||||
|
defer testutil.ExportStatusWriter(seeder, "s", t)()
|
||||||
|
// Just to mix things up, the seeder starts with the data, but the leecher
|
||||||
|
// starts with the metainfo.
|
||||||
|
seederTorrent, err := seeder.AddMagnet(fmt.Sprintf("magnet:?xt=urn:btih:%s", layout.Metainfo.HashInfoBytes().HexString()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
go func() {
|
||||||
|
// Wait until we get the metainfo, then check for the data.
|
||||||
|
<-seederTorrent.GotInfo()
|
||||||
|
seederTorrent.VerifyData()
|
||||||
|
}()
|
||||||
|
cfg = torrent.NewDefaultClientConfig()
|
||||||
|
cfg.DisableTrackers = true
|
||||||
|
cfg.NoDHT = true
|
||||||
|
cfg.DisableTCP = true
|
||||||
|
cfg.DefaultStorage = storage.NewMMap(filepath.Join(layout.BaseDir, "download"))
|
||||||
|
cfg.ListenHost = torrent.LoopbackListenHost
|
||||||
|
cfg.ListenPort = 0
|
||||||
|
leecher, err := torrent.NewClient(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
testutil.ExportStatusWriter(leecher, "l", t)()
|
||||||
|
defer leecher.Close()
|
||||||
|
leecherTorrent, err := leecher.AddTorrent(layout.Metainfo)
|
||||||
|
require.NoError(t, err)
|
||||||
|
leecherTorrent.AddClientPeer(seeder)
|
||||||
|
fs := New(leecher)
|
||||||
|
defer fs.Destroy()
|
||||||
|
root, _ := fs.Root()
|
||||||
|
node, _ := root.(fusefs.NodeStringLookuper).Lookup(context.Background(), "greeting")
|
||||||
|
var attr fuse.Attr
|
||||||
|
node.Attr(context.Background(), &attr)
|
||||||
|
size := attr.Size
|
||||||
|
data := make([]byte, size)
|
||||||
|
h, err := node.(fusefs.NodeOpener).Open(context.TODO(), nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// torrent.Reader.Read no longer tries to fill the entire read buffer, so this is a ReadFull for
|
||||||
|
// fusefs.
|
||||||
|
var n int
|
||||||
|
for n < len(data) {
|
||||||
|
resp := fuse.ReadResponse{Data: data[n:]}
|
||||||
|
err := h.(fusefs.HandleReader).Read(context.Background(), &fuse.ReadRequest{
|
||||||
|
Size: int(size) - n,
|
||||||
|
Offset: int64(n),
|
||||||
|
}, &resp)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
n += len(resp.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.EqualValues(t, testutil.GreetingFileContents, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsSubPath(t *testing.T) {
|
||||||
|
for _, case_ := range []struct {
|
||||||
|
parent, child string
|
||||||
|
is bool
|
||||||
|
}{
|
||||||
|
{"", "", false},
|
||||||
|
{"", "/", true},
|
||||||
|
{"", "a", true},
|
||||||
|
{"a/b", "a/bc", false},
|
||||||
|
{"a/b", "a/b", false},
|
||||||
|
{"a/b", "a/b/c", true},
|
||||||
|
{"a/b", "a//b", false},
|
||||||
|
} {
|
||||||
|
assert.Equal(t, case_.is, isSubPath(case_.parent, case_.child))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
shopt -s nullglob
|
||||||
|
for a in "${TMPDIR:-/tmp}"/torrentfs*; do
|
||||||
|
sudo umount -f "$a/mnt"
|
||||||
|
rm -r -- "$a"
|
||||||
|
done
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"expvar"
|
||||||
|
|
||||||
|
pp "github.com/anacrolix/torrent/peer_protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pieceHash = crypto.SHA1
|
||||||
|
defaultChunkSize = 0x4000 // 16KiB
|
||||||
|
|
||||||
|
// Arbitrary maximum of "metadata_size" (see https://www.bittorrent.org/beps/bep_0009.html)
|
||||||
|
// libtorrent-rasterbar uses 4MiB at last check. TODO: Add links to values used by other
|
||||||
|
// implementations here. I saw 14143527 in the metainfo for
|
||||||
|
// 3597f16e239aeb8f8524a1a1c4e4725a0a96b470. Large values for legitimate torrents should be
|
||||||
|
// recorded here for consideration.
|
||||||
|
maxMetadataSize uint32 = 16 * 1024 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are our extended message IDs. Peers will use these values to
|
||||||
|
// select which extension a message is intended for.
|
||||||
|
const (
|
||||||
|
metadataExtendedId = iota + 1 // 0 is reserved for deleting keys
|
||||||
|
pexExtendedId
|
||||||
|
utHolepunchExtendedId
|
||||||
|
)
|
||||||
|
|
||||||
|
func defaultPeerExtensionBytes() PeerExtensionBits {
|
||||||
|
return pp.NewPeerExtensionBytes(pp.ExtensionBitDht, pp.ExtensionBitLtep, pp.ExtensionBitFast)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
torrent.Set("peers supporting extension", &peersSupportingExtension)
|
||||||
|
torrent.Set("chunks received", &chunksReceived)
|
||||||
|
}
|
||||||
|
|
||||||
|
// I could move a lot of these counters to their own file, but I suspect they
|
||||||
|
// may be attached to a Client someday.
|
||||||
|
var (
|
||||||
|
torrent = expvar.NewMap("torrent")
|
||||||
|
peersSupportingExtension expvar.Map
|
||||||
|
chunksReceived expvar.Map
|
||||||
|
|
||||||
|
pieceHashedCorrect = expvar.NewInt("pieceHashedCorrect")
|
||||||
|
pieceHashedNotCorrect = expvar.NewInt("pieceHashedNotCorrect")
|
||||||
|
|
||||||
|
completedHandshakeConnectionFlags = expvar.NewMap("completedHandshakeConnectionFlags")
|
||||||
|
// Count of connections to peer with same client ID.
|
||||||
|
connsToSelf = expvar.NewInt("connsToSelf")
|
||||||
|
receivedKeepalives = expvar.NewInt("receivedKeepalives")
|
||||||
|
// Requests received for pieces we don't have.
|
||||||
|
requestsReceivedForMissingPieces = expvar.NewInt("requestsReceivedForMissingPieces")
|
||||||
|
requestedChunkLengths = expvar.NewMap("requestedChunkLengths")
|
||||||
|
|
||||||
|
messageTypesReceived = expvar.NewMap("messageTypesReceived")
|
||||||
|
|
||||||
|
// Track the effectiveness of Torrent.connPieceInclinationPool.
|
||||||
|
pieceInclinationsReused = expvar.NewInt("pieceInclinationsReused")
|
||||||
|
pieceInclinationsNew = expvar.NewInt("pieceInclinationsNew")
|
||||||
|
pieceInclinationsPut = expvar.NewInt("pieceInclinationsPut")
|
||||||
|
|
||||||
|
concurrentChunkWrites = expvar.NewInt("torrentConcurrentChunkWrites")
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
module github.com/anacrolix/torrent
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/RoaringBitmap/roaring v1.2.3
|
||||||
|
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0
|
||||||
|
github.com/alexflint/go-arg v1.4.3
|
||||||
|
github.com/anacrolix/bargle v0.0.0-20220630015206-d7a4d433886a
|
||||||
|
github.com/anacrolix/chansync v0.3.0
|
||||||
|
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444
|
||||||
|
github.com/anacrolix/envpprof v1.3.0
|
||||||
|
github.com/anacrolix/fuse v0.2.0
|
||||||
|
github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45
|
||||||
|
github.com/anacrolix/go-libutp v1.3.1
|
||||||
|
github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4
|
||||||
|
github.com/anacrolix/missinggo v1.3.0
|
||||||
|
github.com/anacrolix/missinggo/perf v1.0.0
|
||||||
|
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9
|
||||||
|
github.com/anacrolix/multiless v0.3.0
|
||||||
|
github.com/anacrolix/squirrel v0.6.0
|
||||||
|
github.com/anacrolix/sync v0.5.1
|
||||||
|
github.com/anacrolix/tagflag v1.3.0
|
||||||
|
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96
|
||||||
|
github.com/anacrolix/utp v0.1.0
|
||||||
|
github.com/bahlo/generic-list-go v0.2.0
|
||||||
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8
|
||||||
|
github.com/davecgh/go-spew v1.1.1
|
||||||
|
github.com/dustin/go-humanize v1.0.0
|
||||||
|
github.com/edsrzf/mmap-go v1.1.0
|
||||||
|
github.com/elliotchance/orderedmap v1.4.0
|
||||||
|
github.com/frankban/quicktest v1.14.6
|
||||||
|
github.com/fsnotify/fsnotify v1.5.4
|
||||||
|
github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916
|
||||||
|
github.com/google/btree v1.1.2
|
||||||
|
github.com/google/go-cmp v0.5.9
|
||||||
|
github.com/gorilla/websocket v1.5.0
|
||||||
|
github.com/jessevdk/go-flags v1.5.0
|
||||||
|
github.com/pion/datachannel v1.5.2
|
||||||
|
github.com/pion/logging v0.2.2
|
||||||
|
github.com/pion/webrtc/v3 v3.1.42
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/prometheus/client_golang v1.12.2
|
||||||
|
github.com/stretchr/testify v1.8.1
|
||||||
|
github.com/tidwall/btree v1.6.0
|
||||||
|
go.etcd.io/bbolt v1.3.6
|
||||||
|
go.opentelemetry.io/otel v1.8.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.8.0
|
||||||
|
go.opentelemetry.io/otel/sdk v1.8.0
|
||||||
|
go.opentelemetry.io/otel/trace v1.8.0
|
||||||
|
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df
|
||||||
|
golang.org/x/sys v0.15.0
|
||||||
|
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
||||||
|
github.com/alexflint/go-scalar v1.1.0 // indirect
|
||||||
|
github.com/anacrolix/mmsg v1.0.0 // indirect
|
||||||
|
github.com/anacrolix/stm v0.4.0 // indirect
|
||||||
|
github.com/benbjohnson/immutable v0.3.0 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/bits-and-blooms/bitset v1.2.2 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
|
github.com/go-llsqlite/crawshaw v0.4.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.2.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
|
||||||
|
github.com/huandu/xstrings v1.3.2 // indirect
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
|
github.com/mschoch/smat v0.2.0 // indirect
|
||||||
|
github.com/pion/dtls/v2 v2.2.4 // indirect
|
||||||
|
github.com/pion/ice/v2 v2.2.6 // indirect
|
||||||
|
github.com/pion/interceptor v0.1.11 // indirect
|
||||||
|
github.com/pion/mdns v0.0.5 // indirect
|
||||||
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
|
github.com/pion/rtcp v1.2.9 // indirect
|
||||||
|
github.com/pion/rtp v1.7.13 // indirect
|
||||||
|
github.com/pion/sctp v1.8.2 // indirect
|
||||||
|
github.com/pion/sdp/v3 v3.0.5 // indirect
|
||||||
|
github.com/pion/srtp/v2 v2.0.9 // indirect
|
||||||
|
github.com/pion/stun v0.3.5 // indirect
|
||||||
|
github.com/pion/transport v0.13.1 // indirect
|
||||||
|
github.com/pion/transport/v2 v2.0.0 // indirect
|
||||||
|
github.com/pion/turn/v2 v2.0.8 // indirect
|
||||||
|
github.com/pion/udp v0.1.4 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
|
github.com/prometheus/common v0.35.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.7.3 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||||
|
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
|
||||||
|
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.8.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.8.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v0.18.0 // indirect
|
||||||
|
golang.org/x/crypto v0.17.0 // indirect
|
||||||
|
golang.org/x/net v0.10.0 // indirect
|
||||||
|
golang.org/x/sync v0.3.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 // indirect
|
||||||
|
google.golang.org/grpc v1.46.2 // indirect
|
||||||
|
google.golang.org/protobuf v1.28.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/libc v1.22.3 // indirect
|
||||||
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
modernc.org/memory v1.5.0 // indirect
|
||||||
|
modernc.org/sqlite v1.21.1 // indirect
|
||||||
|
zombiezen.com/go/sqlite v0.13.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
retract (
|
||||||
|
// Doesn't signal interest to peers if choked when piece priorities change.
|
||||||
|
v1.39.0
|
||||||
|
// peer-requesting doesn't scale
|
||||||
|
[v1.34.0, v1.38.1]
|
||||||
|
// Indefinite outgoing requests on storage write errors. https://github.com/anacrolix/torrent/issues/889
|
||||||
|
[v1.29.0, v1.53.2]
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,908 @@
|
||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||||
|
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||||
|
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||||
|
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||||
|
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||||
|
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||||
|
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||||
|
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||||
|
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||||
|
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||||
|
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||||
|
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||||
|
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||||
|
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
|
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||||
|
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||||
|
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||||
|
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||||
|
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||||
|
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||||
|
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||||
|
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||||
|
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||||
|
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||||
|
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||||
|
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||||
|
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||||
|
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||||
|
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||||
|
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||||
|
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk=
|
||||||
|
crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
|
||||||
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
|
||||||
|
github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
|
||||||
|
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
||||||
|
github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
|
||||||
|
github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
|
||||||
|
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||||
|
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||||
|
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0=
|
||||||
|
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k=
|
||||||
|
github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk=
|
||||||
|
github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
|
||||||
|
github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI=
|
||||||
|
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||||
|
github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo=
|
||||||
|
github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA=
|
||||||
|
github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM=
|
||||||
|
github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||||
|
github.com/anacrolix/bargle v0.0.0-20220630015206-d7a4d433886a h1:KCP9QvHlLoUQBOaTf/YCuOzG91Ym1cPB6S68O4Q3puo=
|
||||||
|
github.com/anacrolix/bargle v0.0.0-20220630015206-d7a4d433886a/go.mod h1:9xUiZbkh+94FbiIAL1HXpAIBa832f3Mp07rRPl5c5RQ=
|
||||||
|
github.com/anacrolix/chansync v0.3.0 h1:lRu9tbeuw3wl+PhMu/r+JJCRu5ArFXIluOgdF0ao6/U=
|
||||||
|
github.com/anacrolix/chansync v0.3.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
|
||||||
|
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 h1:8V0K09lrGoeT2KRJNOtspA7q+OMxGwQqK/Ug0IiaaRE=
|
||||||
|
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444/go.mod h1:MctKM1HS5YYDb3F30NGJxLE+QPuqWoT5ReW/4jt8xew=
|
||||||
|
github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
||||||
|
github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
||||||
|
github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
|
||||||
|
github.com/anacrolix/envpprof v1.3.0 h1:WJt9bpuT7A/CDCxPOv/eeZqHWlle/Y0keJUvc6tcJDk=
|
||||||
|
github.com/anacrolix/envpprof v1.3.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0=
|
||||||
|
github.com/anacrolix/fuse v0.2.0 h1:pc+To78kI2d/WUjIyrsdqeJQAesuwpGxlI3h1nAv3Do=
|
||||||
|
github.com/anacrolix/fuse v0.2.0/go.mod h1:Kfu02xBwnySDpH3N23BmrP3MDfwAQGRLUCj6XyeOvBQ=
|
||||||
|
github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45 h1:Kmcl3I9K2+5AdnnR7hvrnVT0TLeFWWMa9bxnm55aVIg=
|
||||||
|
github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
|
||||||
|
github.com/anacrolix/go-libutp v1.3.1 h1:idJzreNLl+hNjGC3ZnUOjujEaryeOGgkwHLqSGoige0=
|
||||||
|
github.com/anacrolix/go-libutp v1.3.1/go.mod h1:heF41EC8kN0qCLMokLBVkB8NXiLwx3t8R8810MTNI5o=
|
||||||
|
github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
|
||||||
|
github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
|
||||||
|
github.com/anacrolix/log v0.10.1-0.20220123034749-3920702c17f8/go.mod h1:GmnE2c0nvz8pOIPUSC9Rawgefy1sDXqposC2wgtBZE4=
|
||||||
|
github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
|
||||||
|
github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4 h1:CdVK9IoqoqklXQQ4+L2aew64xsz14KdOD+rnKdTQajg=
|
||||||
|
github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
|
||||||
|
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62 h1:P04VG6Td13FHMgS5ZBcJX23NPC/fiC4cp9bXwYujdYM=
|
||||||
|
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM=
|
||||||
|
github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s=
|
||||||
|
github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo=
|
||||||
|
github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo=
|
||||||
|
github.com/anacrolix/missinggo v1.2.1/go.mod h1:J5cMhif8jPmFoC3+Uvob3OXXNIhOUikzMt+uUjeM21Y=
|
||||||
|
github.com/anacrolix/missinggo v1.3.0 h1:06HlMsudotL7BAELRZs0yDZ4yVXsHXGi323QBjAVASw=
|
||||||
|
github.com/anacrolix/missinggo v1.3.0/go.mod h1:bqHm8cE8xr+15uVfMG3BFui/TxyB6//H5fwlq/TeqMc=
|
||||||
|
github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy6BqESAJVw=
|
||||||
|
github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ=
|
||||||
|
github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY=
|
||||||
|
github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA=
|
||||||
|
github.com/anacrolix/missinggo/v2 v2.5.2/go.mod h1:yNvsLrtZYRYCOI+KRH/JM8TodHjtIE/bjOGhQaLOWIE=
|
||||||
|
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9 h1:W/oGeHhYwxueeiDjQfmK9G+X9M2xJgfTtow62v0TWAs=
|
||||||
|
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9/go.mod h1:mIEtp9pgaXqt8VQ3NQxFOod/eQ1H0D1XsZzKUQfwtac=
|
||||||
|
github.com/anacrolix/mmsg v0.0.0-20180515031531-a4a3ba1fc8bb/go.mod h1:x2/ErsYUmT77kezS63+wzZp8E3byYB0gzirM/WMBLfw=
|
||||||
|
github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg=
|
||||||
|
github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc=
|
||||||
|
github.com/anacrolix/multiless v0.3.0 h1:5Bu0DZncjE4e06b9r1Ap2tUY4Au0NToBP5RpuEngSis=
|
||||||
|
github.com/anacrolix/multiless v0.3.0/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4=
|
||||||
|
github.com/anacrolix/squirrel v0.6.0 h1:ovfWW42wcGzrVYYI9s56pEYzfeTwtXxCCvSd+KwvUEA=
|
||||||
|
github.com/anacrolix/squirrel v0.6.0/go.mod h1:60vdNPUbK1jYWePp39Wqn9whHm12Yb9JEuwOXzLMDuY=
|
||||||
|
github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
|
||||||
|
github.com/anacrolix/stm v0.4.0 h1:tOGvuFwaBjeu1u9X1eIh9TX8OEedEiEQ1se1FjhFnXY=
|
||||||
|
github.com/anacrolix/stm v0.4.0/go.mod h1:GCkwqWoAsP7RfLW+jw+Z0ovrt2OO7wRzcTtFYMYY5t8=
|
||||||
|
github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk=
|
||||||
|
github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
|
||||||
|
github.com/anacrolix/sync v0.5.1 h1:FbGju6GqSjzVoTgcXTUKkF041lnZkG5P0C3T5RL3SGc=
|
||||||
|
github.com/anacrolix/sync v0.5.1/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
|
||||||
|
github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
|
||||||
|
github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
|
||||||
|
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
|
||||||
|
github.com/anacrolix/tagflag v1.3.0 h1:5NI+9CniDnEH0BWA4UcQbERyFPjKJqZnVkItGVIDy/s=
|
||||||
|
github.com/anacrolix/tagflag v1.3.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
|
||||||
|
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA=
|
||||||
|
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs=
|
||||||
|
github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4=
|
||||||
|
github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk=
|
||||||
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
|
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||||
|
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||||
|
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
||||||
|
github.com/benbjohnson/immutable v0.3.0 h1:TVRhuZx2wG9SZ0LRdqlbs9S5BZ6Y24hJEHTCgWHZEIw=
|
||||||
|
github.com/benbjohnson/immutable v0.3.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
||||||
|
github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Yc15FvIk=
|
||||||
|
github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
||||||
|
github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
||||||
|
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
||||||
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
|
||||||
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
|
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||||
|
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||||
|
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||||
|
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||||
|
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
|
||||||
|
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
|
||||||
|
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||||
|
github.com/elliotchance/orderedmap v1.4.0 h1:wZtfeEONCbx6in1CZyE6bELEt/vFayMvsxqI5SgsR+A=
|
||||||
|
github.com/elliotchance/orderedmap v1.4.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
|
||||||
|
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||||
|
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||||
|
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||||
|
github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||||
|
github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||||
|
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||||
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
|
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||||
|
github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 h1:OyQmpAN302wAopDgwVjgs2HkFawP9ahIEqkUYz7V7CA=
|
||||||
|
github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916/go.mod h1:DADrR88ONKPPeSGjFp5iEN55Arx3fi2qXZeKCYDpbmU=
|
||||||
|
github.com/go-llsqlite/crawshaw v0.4.0 h1:L02s2jZBBJj80xm1VkkdyB/JlQ/Fi0kLbNHfXA8yrec=
|
||||||
|
github.com/go-llsqlite/crawshaw v0.4.0/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||||
|
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||||
|
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
|
||||||
|
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||||
|
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||||
|
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
|
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
|
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
||||||
|
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
|
||||||
|
github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
|
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
|
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
|
||||||
|
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||||
|
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||||
|
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
|
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||||
|
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||||
|
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
|
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||||
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||||
|
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||||
|
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||||
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
|
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
|
||||||
|
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
|
||||||
|
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
|
||||||
|
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
|
||||||
|
github.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg=
|
||||||
|
github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=
|
||||||
|
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
|
||||||
|
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
|
||||||
|
github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
|
||||||
|
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
|
||||||
|
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||||
|
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
|
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
||||||
|
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
|
||||||
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
|
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
|
||||||
|
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
|
||||||
|
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
||||||
|
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||||
|
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||||
|
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
|
||||||
|
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||||
|
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
|
||||||
|
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||||
|
github.com/pion/srtp/v2 v2.0.9 h1:JJq3jClmDFBPX/F5roEb0U19jSU7eUhyDqR/NZ34EKQ=
|
||||||
|
github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
|
||||||
|
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
||||||
|
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
||||||
|
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
||||||
|
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
|
||||||
|
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
|
||||||
|
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
|
||||||
|
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
|
||||||
|
github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4=
|
||||||
|
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
|
||||||
|
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
|
||||||
|
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
|
||||||
|
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
||||||
|
github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8=
|
||||||
|
github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
|
||||||
|
github.com/pion/webrtc/v3 v3.1.42 h1:wJEQFIXVanptnQcHOLTuIo4AtGB2+mG2x4OhIhnITOA=
|
||||||
|
github.com/pion/webrtc/v3 v3.1.42/go.mod h1:ffD9DulDrPxyWvDPUIPAOSAWx9GUlOExiJPf7cCcMLA=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||||
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
|
github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||||
|
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||||
|
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||||
|
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||||
|
github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
|
||||||
|
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||||
|
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||||
|
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||||
|
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||||
|
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||||
|
github.com/prometheus/common v0.35.0 h1:Eyr+Pw2VymWejHqCugNaQXkAi6KayVNxaHeu6khmFBE=
|
||||||
|
github.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||||
|
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||||
|
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||||
|
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
|
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||||
|
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s=
|
||||||
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs=
|
||||||
|
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
|
||||||
|
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
|
||||||
|
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
||||||
|
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||||
|
github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg=
|
||||||
|
github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
|
||||||
|
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
|
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
|
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
|
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ=
|
||||||
|
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
|
||||||
|
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
|
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||||
|
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||||
|
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||||
|
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||||
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opentelemetry.io/otel v1.8.0 h1:zcvBFizPbpa1q7FehvFiHbQwGzmPILebO0tyqIR5Djg=
|
||||||
|
go.opentelemetry.io/otel v1.8.0/go.mod h1:2pkj+iMj0o03Y+cW6/m8Y4WkRdYN3AvCXCnzRMp9yvM=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.8.0 h1:ao8CJIShCaIbaMsGxy+jp2YHSudketpDgDRcbirov78=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.8.0/go.mod h1:78XhIg8Ht9vR4tbLNUhXsiOnE2HOuSeKAiAcoVQEpOY=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.8.0 h1:LrHL1A3KqIgAgi6mK7Q0aczmzU414AONAGT5xtnp+uo=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.8.0/go.mod h1:w8aZL87GMOvOBa2lU/JlVXE1q4chk/0FX+8ai4513bw=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.8.0 h1:00hCSGLIxdYK/Z7r8GkaX0QIlfvgU3tmnLlQvcnix6U=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.8.0/go.mod h1:twhIvtDQW2sWP1O2cT1N8nkSBgKCRZv2z6COTTBrf8Q=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.8.0 h1:xwu69/fNuwbSHWe/0PGS888RmjWY181OmcXDQKu7ZQk=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.8.0/go.mod h1:uPSfc+yfDH2StDM/Rm35WE8gXSNdvCg023J6HeGNO0c=
|
||||||
|
go.opentelemetry.io/otel/trace v1.8.0 h1:cSy0DF9eGI5WIfNwZ1q2iUyGj00tGzP24dE1lOlHrfY=
|
||||||
|
go.opentelemetry.io/otel/trace v1.8.0/go.mod h1:0Bt3PXY8w+3pheS3hQUt+wow8b1ojPaTBoTCh2zIFI4=
|
||||||
|
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||||
|
go.opentelemetry.io/proto/otlp v0.18.0 h1:W5hyXNComRa23tGpKwG+FRAc4rfF6ZUg1JReK+QHS80=
|
||||||
|
go.opentelemetry.io/proto/otlp v0.18.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||||
|
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||||
|
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||||
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
|
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||||
|
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||||
|
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||||
|
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
|
||||||
|
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||||
|
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||||
|
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
|
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
|
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
|
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
|
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||||
|
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
|
||||||
|
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||||
|
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||||
|
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
|
golang.org/x/tools v0.0.0-20200423201157-2723c5de0d66/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||||
|
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||||
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
|
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||||
|
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||||
|
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||||
|
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||||
|
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||||
|
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||||
|
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 h1:b9mVrqYfq3P4bCdaLg1qtBnPzUYgglsIdjZkL/fQVOE=
|
||||||
|
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||||
|
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||||
|
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||||
|
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||||
|
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||||
|
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||||
|
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||||
|
google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
|
||||||
|
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||||
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
|
||||||
|
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
|
||||||
|
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||||
|
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
|
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||||
|
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
|
modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
|
||||||
|
modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
|
||||||
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
|
zombiezen.com/go/sqlite v0.13.1 h1:qDzxyWWmMtSSEH5qxamqBFmqA2BLSSbtODi3ojaE02o=
|
||||||
|
zombiezen.com/go/sqlite v0.13.1/go.mod h1:Ht/5Rg3Ae2hoyh1I7gbWtWAl89CNocfqeb/aAMTkJr4=
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/mse"
|
||||||
|
pp "github.com/anacrolix/torrent/peer_protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wraps a raw connection and provides the interface we want for using the
|
||||||
|
// connection in the message loop.
|
||||||
|
type deadlineReader struct {
|
||||||
|
nc net.Conn
|
||||||
|
r io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r deadlineReader) Read(b []byte) (int, error) {
|
||||||
|
// Keep-alives should be received every 2 mins. Give a bit of gracetime.
|
||||||
|
err := r.nc.SetReadDeadline(time.Now().Add(150 * time.Second))
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("error setting read deadline: %s", err)
|
||||||
|
}
|
||||||
|
return r.r.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles stream encryption for inbound connections.
|
||||||
|
func handleEncryption(
|
||||||
|
rw io.ReadWriter,
|
||||||
|
skeys mse.SecretKeyIter,
|
||||||
|
policy HeaderObfuscationPolicy,
|
||||||
|
selector mse.CryptoSelector,
|
||||||
|
) (
|
||||||
|
ret io.ReadWriter,
|
||||||
|
headerEncrypted bool,
|
||||||
|
cryptoMethod mse.CryptoMethod,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
// Tries to start an unencrypted stream.
|
||||||
|
if !policy.RequirePreferred || !policy.Preferred {
|
||||||
|
var protocol [len(pp.Protocol)]byte
|
||||||
|
_, err = io.ReadFull(rw, protocol[:])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Put the protocol back into the stream.
|
||||||
|
rw = struct {
|
||||||
|
io.Reader
|
||||||
|
io.Writer
|
||||||
|
}{
|
||||||
|
io.MultiReader(bytes.NewReader(protocol[:]), rw),
|
||||||
|
rw,
|
||||||
|
}
|
||||||
|
if string(protocol[:]) == pp.Protocol {
|
||||||
|
ret = rw
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if policy.RequirePreferred {
|
||||||
|
// We are here because we require unencrypted connections.
|
||||||
|
err = fmt.Errorf("unexpected protocol string %q and header obfuscation disabled", protocol)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headerEncrypted = true
|
||||||
|
ret, cryptoMethod, err = mse.ReceiveHandshake(rw, skeys, selector)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerExtensionBits = pp.PeerExtensionBits
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package torrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultExtensionBytes(t *testing.T) {
|
||||||
|
pex := defaultPeerExtensionBytes()
|
||||||
|
assert.True(t, pex.SupportsDHT())
|
||||||
|
assert.True(t, pex.SupportsExtended())
|
||||||
|
assert.False(t, pex.GetBit(63))
|
||||||
|
assert.Panics(t, func() { pex.GetBit(64) })
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package alloclim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/anacrolix/envpprof"
|
||||||
|
qt "github.com/frankban/quicktest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReserveOverMax(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
l := &Limiter{Max: 10}
|
||||||
|
r := l.Reserve(20)
|
||||||
|
c.Assert(r.Wait(context.Background()), qt.IsNotNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImmediateAllow(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
l := &Limiter{Max: 10}
|
||||||
|
r := l.Reserve(10)
|
||||||
|
c.Assert(r.Wait(context.Background()), qt.IsNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleSequence(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
l := &Limiter{Max: 10}
|
||||||
|
rs := make([]*Reservation, 0)
|
||||||
|
rs = append(rs, l.Reserve(6))
|
||||||
|
rs = append(rs, l.Reserve(5))
|
||||||
|
rs = append(rs, l.Reserve(5))
|
||||||
|
c.Assert(rs[0].Wait(context.Background()), qt.IsNil)
|
||||||
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Nanosecond))
|
||||||
|
c.Assert(rs[1].Wait(ctx), qt.Equals, context.DeadlineExceeded)
|
||||||
|
go cancel()
|
||||||
|
ctx, cancel = context.WithCancel(context.Background())
|
||||||
|
go cancel()
|
||||||
|
c.Assert(rs[2].Wait(ctx), qt.Equals, context.Canceled)
|
||||||
|
go rs[0].Release()
|
||||||
|
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(time.Second))
|
||||||
|
c.Assert(rs[1].Wait(ctx), qt.IsNil)
|
||||||
|
go rs[1].Release()
|
||||||
|
c.Assert(rs[2].Wait(ctx), qt.IsNil)
|
||||||
|
go rs[2].Release()
|
||||||
|
go cancel()
|
||||||
|
rs[2].Release()
|
||||||
|
rs[1].Release()
|
||||||
|
c.Assert(l.Value(), qt.Equals, l.Max)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSequenceWithCancel(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
l := &Limiter{Max: 10}
|
||||||
|
rs := make([]*Reservation, 0)
|
||||||
|
rs = append(rs, l.Reserve(6))
|
||||||
|
rs = append(rs, l.Reserve(6))
|
||||||
|
rs = append(rs, l.Reserve(4))
|
||||||
|
rs = append(rs, l.Reserve(4))
|
||||||
|
c.Assert(rs[0].Cancel(), qt.IsFalse)
|
||||||
|
c.Assert(func() { rs[1].Release() }, qt.PanicMatches, "not resolved")
|
||||||
|
c.Assert(rs[1].Cancel(), qt.IsTrue)
|
||||||
|
c.Assert(rs[2].Wait(context.Background()), qt.IsNil)
|
||||||
|
rs[0].Release()
|
||||||
|
c.Assert(rs[3].Wait(context.Background()), qt.IsNil)
|
||||||
|
c.Assert(l.Value(), qt.Equals, int64(2))
|
||||||
|
rs[1].Release()
|
||||||
|
rs[2].Release()
|
||||||
|
rs[3].Release()
|
||||||
|
c.Assert(l.Value(), qt.Equals, l.Max)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCancelWhileWaiting(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
l := &Limiter{Max: 10}
|
||||||
|
rs := make([]*Reservation, 0)
|
||||||
|
rs = append(rs, l.Reserve(6))
|
||||||
|
rs = append(rs, l.Reserve(6))
|
||||||
|
rs = append(rs, l.Reserve(4))
|
||||||
|
rs = append(rs, l.Reserve(4))
|
||||||
|
go rs[1].Cancel()
|
||||||
|
err := rs[1].Wait(context.Background())
|
||||||
|
c.Assert(err, qt.IsNotNil)
|
||||||
|
err = rs[2].Wait(context.Background())
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
go cancel()
|
||||||
|
err = rs[3].Wait(ctx)
|
||||||
|
c.Assert(err, qt.Equals, context.Canceled)
|
||||||
|
rs[0].Drop()
|
||||||
|
err = rs[3].Wait(ctx)
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package alloclim
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
// Manages reservations sharing a common allocation limit.
|
||||||
|
type Limiter struct {
|
||||||
|
// Maximum outstanding allocation space.
|
||||||
|
Max int64
|
||||||
|
initOnce sync.Once
|
||||||
|
mu sync.Mutex
|
||||||
|
// Current unallocated space.
|
||||||
|
value int64
|
||||||
|
// Reservations waiting to in the order they arrived.
|
||||||
|
waiting []*Reservation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *Limiter) initValue() {
|
||||||
|
me.value = me.Max
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *Limiter) init() {
|
||||||
|
me.initOnce.Do(func() {
|
||||||
|
me.initValue()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *Limiter) Reserve(n int64) *Reservation {
|
||||||
|
r := &Reservation{
|
||||||
|
l: me,
|
||||||
|
n: n,
|
||||||
|
}
|
||||||
|
me.init()
|
||||||
|
me.mu.Lock()
|
||||||
|
if n <= me.value {
|
||||||
|
me.value -= n
|
||||||
|
r.granted.Set()
|
||||||
|
} else {
|
||||||
|
me.waiting = append(me.waiting, r)
|
||||||
|
}
|
||||||
|
me.mu.Unlock()
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *Limiter) doWakesLocked() {
|
||||||
|
for {
|
||||||
|
if len(me.waiting) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
r := me.waiting[0]
|
||||||
|
switch {
|
||||||
|
case r.cancelled.IsSet():
|
||||||
|
case r.n <= me.value:
|
||||||
|
if r.wake() {
|
||||||
|
me.value -= r.n
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
me.waiting = me.waiting[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *Limiter) doWakes() {
|
||||||
|
me.mu.Lock()
|
||||||
|
me.doWakesLocked()
|
||||||
|
me.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *Limiter) addValue(n int64) {
|
||||||
|
me.mu.Lock()
|
||||||
|
me.value += n
|
||||||
|
me.doWakesLocked()
|
||||||
|
me.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *Limiter) Value() int64 {
|
||||||
|
me.mu.Lock()
|
||||||
|
defer me.mu.Unlock()
|
||||||
|
return me.value
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
package alloclim
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/anacrolix/chansync"
|
||||||
|
"github.com/anacrolix/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Reservation struct {
|
||||||
|
l *Limiter
|
||||||
|
n int64
|
||||||
|
releaseOnce sync.Once
|
||||||
|
mu sync.Mutex
|
||||||
|
granted chansync.SetOnce
|
||||||
|
cancelled chansync.SetOnce
|
||||||
|
}
|
||||||
|
|
||||||
|
// Releases the alloc claim if the reservation has been granted. Does nothing if it was cancelled.
|
||||||
|
// Otherwise panics.
|
||||||
|
func (me *Reservation) Release() {
|
||||||
|
me.mu.Lock()
|
||||||
|
defer me.mu.Unlock()
|
||||||
|
switch {
|
||||||
|
default:
|
||||||
|
panic("not resolved")
|
||||||
|
case me.cancelled.IsSet():
|
||||||
|
return
|
||||||
|
case me.granted.IsSet():
|
||||||
|
}
|
||||||
|
me.releaseOnce.Do(func() {
|
||||||
|
me.l.addValue(me.n)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel the reservation, returns false if it was already granted. You must still release if that's
|
||||||
|
// the case. See Drop.
|
||||||
|
func (me *Reservation) Cancel() bool {
|
||||||
|
me.mu.Lock()
|
||||||
|
defer me.mu.Unlock()
|
||||||
|
if me.granted.IsSet() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if me.cancelled.Set() {
|
||||||
|
go me.l.doWakes()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the reservation is granted, release it, otherwise cancel the reservation.
|
||||||
|
func (me *Reservation) Drop() {
|
||||||
|
me.mu.Lock()
|
||||||
|
defer me.mu.Unlock()
|
||||||
|
if me.granted.IsSet() {
|
||||||
|
me.releaseOnce.Do(func() {
|
||||||
|
me.l.addValue(me.n)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if me.cancelled.Set() {
|
||||||
|
go me.l.doWakes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *Reservation) wake() bool {
|
||||||
|
me.mu.Lock()
|
||||||
|
defer me.mu.Unlock()
|
||||||
|
if me.cancelled.IsSet() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return me.granted.Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (me *Reservation) Wait(ctx context.Context) error {
|
||||||
|
if me.n > me.l.Max {
|
||||||
|
return log.WithLevel(
|
||||||
|
log.Warning,
|
||||||
|
fmt.Errorf("reservation for %v exceeds limiter max %v", me.n, me.l.Max),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-me.granted.Done():
|
||||||
|
case <-me.cancelled.Done():
|
||||||
|
}
|
||||||
|
defer me.mu.Unlock()
|
||||||
|
me.mu.Lock()
|
||||||
|
switch {
|
||||||
|
case me.granted.IsSet():
|
||||||
|
return nil
|
||||||
|
case me.cancelled.IsSet():
|
||||||
|
return errors.New("reservation cancelled")
|
||||||
|
case ctx.Err() != nil:
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
panic("unexpected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package check
|
||||||
|
|
||||||
|
// A flag for doing extra checks at runtime that are potentially expensive. Should be enabled for
|
||||||
|
// testing and debugging.
|
||||||
|
var Enabled = false
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue