Skip to content

Commit

Permalink
it works
Browse files Browse the repository at this point in the history
  • Loading branch information
ninehills committed Jan 3, 2022
1 parent 20b717c commit d31f51a
Show file tree
Hide file tree
Showing 10 changed files with 2,485 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@

# Dependency directories (remove the comment below to include it)
# vendor/
#
*.torrent.db*
95 changes: 93 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,93 @@
# p2pfile
p2pfile - DHT-based P2P file distribution command line tools.
# p2pfile - DHT-based P2P file distribution command line tools

## 背景

### 应用场景

- 所有节点网络联通的环境下的文件分布式分发。
- 私有网络环境,和互联网隔离。
- 无文件加密传输需求

### 设计限制

- 不支持 Tracker,只支持 DHT 网络,从而简化设计。
- 不需要 Daemon 常驻进程,只需要单个二进制文件。
- 无加密设计
- 只支持单个文件分发,不支持文件夹分发。
- 不支持 IPv6。

### 设计目标

- 提供私有网络环境下的文件分布式分发。
- 提供最简化的使用方法,一条命令。

## 命令行设计

```txt
DHT-based P2P file distribution command line tools. For example:
p2pfile serve <FILE_PATH1> <FILE_PATH2> ...
p2pfile download <MAGNET_URI> <MAGNET_URI2> ...
Usage:
p2pfile [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
download Download file from magnet uri.
help Help about any command
serve creates and seeds a torrent from filepaths.
Flags:
--config string config file (default is $HOME/.p2pfile.yaml)
--ip string Set ip. (default: default route ip)
--port int Set port. (default: random port in port-range, See --port-range)
--port-range string Set random port range. (default: 42070-42099) (default "42070-42099")
--peers strings Set bootstrap peers. (default: empty) (eg: --peers 10.1.1.1:2233,10.2.2.2:4567
--upload-limit float Set upload limit, MiB. (default: 0.0)
--download-limit float Set download limit, MiB. (default: 0.0)
--debug Debug mode.
-h, --help help for p2pfile
Use "p2pfile [command] --help" for more information about a command.
```

## 其他设计

A. Magnet URI schema(使用了[BEP0009](http://www.bittorrent.org/beps/bep_0009.html) 扩展)

`magnet:?xt=urn:btih:<info-hash>&dn=<name>&x.pe=<peer-address>`

- `info-hash`: 哈希值,用于标识文件
- `name`: 文件名[可选]
- `peer-address`: 做种机器的Peer地址,用于初始化 DHT 网络

一台机器并发做种或者下载:

- 由于无后台进程,每个进程都是单独的客户端,均需要占用一个端口。
- 故端口需要支持随机分配,从而避免端口冲突。
- 随机分配方法:在端口段中随机选择端口,如果端口被占用,则重新随机选择。
- 随机分配存在当做种进程重启后,其新的端口号和之前不同。
- 方法1:将端口号持久化,当重新serve的时候,使用之前的端口号
- 方法2:重新做种的时候,需要传入magnet uri,从中解析出端口号。

B. 做种高可用性:

- 可以使用2+台机器同时做种,做种时增加参数:`--peers=<peer1>,<peer2>` 从而保证Magnet URI的高可用性。

C. 下载后持续做种:

- 增加参数 `--seeding`,指定下载之后持续做种。持续做种结束条件有多个,任意条件满足即停止做种。
- `--seeding-time=<time>`,指定持续做种的时间,单位为秒。默认为 60s。
- `--seeding-ratio=<ratio>`,指定持续做种的种子分享率,单位为百分比。默认为 1.0。
- 该参数推荐在大文件分发时使用,小文件分发没必要且不建议使用。

D. 信息存储:

- 默认为 SQlite,会在当前目录下生成 `.*db.*` 文件。
- (后续支持) 支持更换存储后端,比如 bolt/sqlite/file etc.

E. 库:

- <https://github.com/anacrolix/torrent>: 主要使用.
- <https://gitlab.com/axet/libtorrent>: 后续参考实现 14: Local Peers Discovery / 19: WebSeeds.
63 changes: 63 additions & 0 deletions cmd/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package cmd

import (
"context"
"os"
"os/signal"

"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/ninehills/p2pfile/pkg/libtorrent"

log "github.com/sirupsen/logrus"
)

func newDownloadCmd() *cobra.Command {
var downloadCmd = &cobra.Command{
Use: "download",
Short: "Download file from magnet uri.",
Long: `Download file from magnet uri. Usage:
p2pfile download <MAGNET_URI> <MAGNET_URI2> ...`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// TODO: 写入这里才生效,需要改进
initLogger(viper.GetBool("debug"))
// TODO: support set download directory
downloader, err := libtorrent.NewTorrentServer(
"download", viper.GetString("ip"), viper.GetInt("port"), viper.GetString("port-range"),
viper.GetStringSlice("peers"), viper.GetFloat64("upload-limit"),
viper.GetFloat64("download-limit"), viper.GetBool("debug"),
args, []string{},
)
if err != nil {
log.Fatal("Failed to create downloader: ", err)
}
ctx := context.Background()

// trap Ctrl+C and call cancel on the context
ctx, cancel := context.WithCancel(ctx)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
defer func() {
signal.Stop(c)
cancel()
}()
go func() {
select {
case sig := <-c:
log.Errorf("Caught signal %v, shutting down...\n", sig)
cancel()
case <-ctx.Done():
}
}()
err = downloader.RunDownloader(ctx)
if err != nil {
log.Fatal("Failed to run downloader: ", err)
}
},
}
downloadCmd.MarkFlagRequired("magnet")
return downloadCmd
}
99 changes: 99 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package cmd

import (
"fmt"
"os"

log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var cfgFile string

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "p2pfile",
Short: "DHT-based P2P file distribution command line tools",
Long: `DHT-based P2P file distribution command line tools. For example:
p2pfile serve <FILE_PATH1> <FILE_PATH2> ...
p2pfile download <MAGNET_URI> <MAGNET_URI2> ...`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}

func init() {
cobra.OnInitialize(initConfig)

rootCmd.Flags().SortFlags = false
rootCmd.PersistentFlags().SortFlags = false

rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.p2pfile.yaml)")
rootCmd.PersistentFlags().String("ip", "", "Set ip. (default: default route ip)")
rootCmd.PersistentFlags().Int("port", 0, "Set port. (default: random port in port-range, See --port-range)")
rootCmd.PersistentFlags().String("port-range", "42070-42099", "Set random port range. (default: 42070-42099)")
rootCmd.PersistentFlags().StringSlice("peers", []string{}, "Set bootstrap peers. (default: empty) (eg: --peers 10.1.1.1:2233,10.2.2.2:4567")
rootCmd.PersistentFlags().Float64("upload-limit", 0.0, "Set upload limit, MiB. (default: 0.0)")
rootCmd.PersistentFlags().Float64("download-limit", 0.0, "Set download limit, MiB. (default: 0.0)")
rootCmd.PersistentFlags().Bool("debug", false, "Debug mode.")

viper.BindPFlag("ip", rootCmd.PersistentFlags().Lookup("ip"))
viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port"))
viper.BindPFlag("port-range", rootCmd.PersistentFlags().Lookup("port-range"))
viper.BindPFlag("peers", rootCmd.PersistentFlags().Lookup("peers"))
viper.BindPFlag("upload-limit", rootCmd.PersistentFlags().Lookup("upload-limit"))
viper.BindPFlag("download-limit", rootCmd.PersistentFlags().Lookup("download-limit"))
viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug"))

rootCmd.AddCommand(newServeCmd())
rootCmd.AddCommand(newDownloadCmd())
}

func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := os.UserHomeDir()
cobra.CheckErr(err)

// Search config in home directory with name ".cobra" (without extension).
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".p2pfile")
}

viper.AutomaticEnv()

if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}

func initLogger(debug bool) {
// Log as JSON instead of the default ASCII formatter.
// log.SetFormatter(&log.JSONFormatter{})

// Output to stdout instead of the default stderr
// Can be any io.Writer, see below for File example
log.SetOutput(os.Stdout)

if debug {
log.Infof("Logging level set to debug.")
log.SetLevel(log.DebugLevel)
} else {
log.SetLevel(log.InfoLevel)
}
}
60 changes: 60 additions & 0 deletions cmd/serve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package cmd

import (
"context"
"os"
"os/signal"

"github.com/ninehills/p2pfile/pkg/libtorrent"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

func newServeCmd() *cobra.Command {
var serveCmd = &cobra.Command{
Use: "serve",
Short: "creates and seeds a torrent from filepaths.",
Long: `DHT-based P2P file distribution command line tools. Usage:
p2pfile serve <FILE_PATH1> <FILE_PATH2> ...`,
Run: func(cmd *cobra.Command, args []string) {
// TODO: 写入这里才生效,需要改进
initLogger(viper.GetBool("debug"))
server, err := libtorrent.NewTorrentServer(
"serve", viper.GetString("ip"), viper.GetInt("port"), viper.GetString("port-range"),
viper.GetStringSlice("peers"), viper.GetFloat64("upload-limit"),
viper.GetFloat64("download-limit"), viper.GetBool("debug"),
[]string{}, args,
)
if err != nil {
log.Fatal("Failed to create server: ", err)
}

ctx := context.Background()

// trap Ctrl+C and call cancel on the context
ctx, cancel := context.WithCancel(ctx)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
defer func() {
signal.Stop(c)
cancel()
}()
go func() {
select {
case sig := <-c:
log.Errorf("Caught signal %v, shutting down...\n", sig)
cancel()
case <-ctx.Done():
}
}()
err = server.RunServer(ctx)
if err != nil {
log.Fatal("Failed to run server: ", err)
}
},
}
serveCmd.MarkFlagRequired("files")
return serveCmd
}
Loading

0 comments on commit d31f51a

Please sign in to comment.