├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .travis.yml
├── LICENSE
├── Makefile
├── README.md
├── api
├── files.go
├── routes.go
├── service.go
├── settings.go
└── torrents.go
├── bittorrent
├── errors.go
├── file.go
├── reader.go
├── service.go
├── service_all.go
├── service_arm.go
├── torrent.go
└── utils.go
├── diskusage
├── diskuage_posix.go
├── diskusage.go
└── diskusage_windows.go
├── docs
├── docs.go
├── swagger.json
└── swagger.yaml
├── go.mod
├── go.sum
├── main.go
├── platform_host.mk
├── platform_target.mk
├── settings
└── settings.go
└── util
├── hash.go
└── version.go
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 | on:
3 | push:
4 | branches:
5 | - '*'
6 | pull_request:
7 | branches:
8 | - '*'
9 | create:
10 | tags:
11 | - v*
12 |
13 | jobs:
14 | build:
15 | name: Build
16 | runs-on: ubuntu-latest
17 | strategy:
18 | max-parallel: 5
19 | matrix:
20 | platform: [
21 | android-arm, android-arm64, android-x64, android-x86,
22 | linux-armv7, linux-arm64, linux-x64, linux-x86,
23 | windows-x64, windows-x86, darwin-x64 ]
24 | steps:
25 | - name: Checkout
26 | uses: actions/checkout@v2
27 |
28 | - name: Setup Go
29 | uses: actions/setup-go@v2
30 | with:
31 | go-version: 1.16.2
32 |
33 | - name: Build ${{ matrix.platform }}
34 | run: make ${{ matrix.platform }}
35 |
36 | - name: Generate ${{ matrix.platform }} zip
37 | id: generate_zip
38 | run: |
39 | make zip PLATFORM=${{ matrix.platform }}
40 | asset_path=$(ls build/binaries/*.zip)
41 | echo "::set-output name=asset_path::${asset_path}"
42 | echo "::set-output name=asset_name::$(basename "${asset_path}")"
43 |
44 | - name: Upload ${{ matrix.platform }} zip
45 | uses: actions/upload-artifact@v2
46 | with:
47 | name: ${{ matrix.platform }}
48 | path: ${{ steps.generate_zip.outputs.asset_path }}
49 |
50 | release:
51 | name: Release
52 | runs-on: ubuntu-latest
53 | needs: build
54 | if: success() && github.event_name == 'create' && startsWith(github.ref, 'refs/tags/')
55 | steps:
56 | - name: Checkout
57 | uses: actions/checkout@v2
58 |
59 | - name: Get all artifacts
60 | uses: actions/download-artifact@v2
61 | with:
62 | path: artifacts
63 |
64 | - name: Perform release
65 | run: |
66 | assets=()
67 | for asset in artifacts/*/*.zip; do
68 | assets+=("-a" "${asset}")
69 | done
70 | git_tag="$(sed 's|refs/tags/||' <<< "${{ github.ref }}")"
71 | hub release create "${assets[@]}" -m "${git_tag}" "${git_tag}"
72 | env:
73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
74 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | build
3 | settings.json
4 | downloads
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: xenial
2 | os: linux
3 | language: go
4 | go:
5 | - "1.14"
6 |
7 | env:
8 | - PLATFORM=android-arm
9 | - PLATFORM=android-arm64
10 | - PLATFORM=android-x64
11 | - PLATFORM=android-x86
12 | - PLATFORM=darwin-x64
13 | - PLATFORM=linux-armv7
14 | - PLATFORM=linux-arm64
15 | - PLATFORM=linux-x64
16 | - PLATFORM=linux-x86
17 | - PLATFORM=windows-x64
18 | - PLATFORM=windows-x86
19 |
20 | services:
21 | - docker
22 |
23 | install:
24 | - go get -d ./...
25 |
26 | script:
27 | - set -e
28 | - make libs
29 | - make ${PLATFORM}
30 |
31 | before_deploy:
32 | - |
33 | if [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ -n "${TRAVIS_TAG}" ]; then
34 | make zip PLATFORM=${PLATFORM}
35 | fi
36 |
37 | deploy:
38 | provider: releases
39 | api_key: ${GH_TOKEN}
40 | file_glob: true
41 | file: build/binaries/*.zip
42 | skip_cleanup: true
43 | on:
44 | tags: true
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 i96751414
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | CC = cc
2 | CXX = c++
3 | STRIP = strip
4 |
5 | PROJECT = i96751414
6 | NAME = torrest
7 | GO_PKG = github.com/i96751414/torrest
8 | GO = go
9 | DOCKER = docker
10 | LIBTORRENT_TAG = 1.2.14-0
11 | UPX = upx
12 | CGO_ENABLED = 1
13 | BUILD_DIR = build
14 | LIBTORRENT_GO = github.com/i96751414/libtorrent-go
15 | GIT = git
16 | GIT_VERSION = $(shell $(GIT) describe --tags | cut -c2-)
17 | ifeq ($(GIT_VERSION),)
18 | GIT_VERSION := dev
19 | endif
20 |
21 | PLATFORMS = \
22 | android-arm \
23 | android-arm64 \
24 | android-x64 \
25 | android-x86 \
26 | darwin-x64 \
27 | linux-arm \
28 | linux-armv7 \
29 | linux-arm64 \
30 | linux-x64 \
31 | linux-x86 \
32 | windows-x64 \
33 | windows-x86
34 |
35 | ifeq ($(GOPATH),)
36 | GOPATH := $(shell go env GOPATH)
37 | endif
38 |
39 | include platform_host.mk
40 |
41 | ifneq ($(CROSS_TRIPLE),)
42 | CC := $(CROSS_TRIPLE)-$(CC)
43 | CXX := $(CROSS_TRIPLE)-$(CXX)
44 | STRIP := $(CROSS_TRIPLE)-strip
45 | endif
46 |
47 | include platform_target.mk
48 |
49 | ifeq ($(TARGET_ARCH), x86)
50 | GOARCH = 386
51 | else ifeq ($(TARGET_ARCH), x64)
52 | GOARCH = amd64
53 | else ifeq ($(TARGET_ARCH), arm)
54 | GOARCH = arm
55 | GOARM = 6
56 | else ifeq ($(TARGET_ARCH), armv7)
57 | GOARCH = arm
58 | GOARM = 7
59 | PKGDIR = -pkgdir $(GOPATH)/pkg/linux_armv7
60 | else ifeq ($(TARGET_ARCH), arm64)
61 | GOARCH = arm64
62 | GOARM =
63 | endif
64 |
65 | ifeq ($(TARGET_OS), windows)
66 | EXT = .exe
67 | GOOS = windows
68 | # TODO Remove for golang 1.8
69 | # https://github.com/golang/go/issues/8756
70 | GO_LDFLAGS = -extldflags=-Wl,--allow-multiple-definition -v
71 | else ifeq ($(TARGET_OS), darwin)
72 | EXT =
73 | GOOS = darwin
74 | # Needs this or cgo will try to link with libgcc, which will fail
75 | CC := $(CROSS_ROOT)/bin/$(CROSS_TRIPLE)-clang
76 | CXX := $(CROSS_ROOT)/bin/$(CROSS_TRIPLE)-clang++
77 | GO_LDFLAGS = -linkmode=external -extld=$(CC)
78 | else ifeq ($(TARGET_OS), linux)
79 | EXT =
80 | GOOS = linux
81 | GO_LDFLAGS = -linkmode=external -extld=$(CC)
82 | else ifeq ($(TARGET_OS), android)
83 | EXT =
84 | GOOS = android
85 | ifeq ($(TARGET_ARCH), arm)
86 | GOARM = 7
87 | else
88 | GOARM =
89 | endif
90 | GO_LDFLAGS = -linkmode=external -extldflags=-pie -extld=$(CC)
91 | CC := $(CROSS_ROOT)/bin/$(CROSS_TRIPLE)-clang
92 | CXX := $(CROSS_ROOT)/bin/$(CROSS_TRIPLE)-clang++
93 | endif
94 |
95 | GO_LDFLAGS += -w -X $(GO_PKG)/util.Version=$(GIT_VERSION)
96 |
97 | DOCKER_GOPATH = "/go"
98 | DOCKER_WORKDIR = "$(DOCKER_GOPATH)/src/$(GO_PKG)"
99 | DOCKER_GOCACHE = "/tmp/.cache"
100 |
101 | WORKDIR = $(shell pwd)
102 |
103 | OUTPUT_NAME = $(NAME)$(EXT)
104 | BUILD_PATH = $(BUILD_DIR)/$(TARGET_OS)_$(TARGET_ARCH)
105 | # LIBTORRENT_GO_HOME = "$(GOPATH)/src/$(LIBTORRENT_GO)"
106 | LIBTORRENT_GO_HOME = "$$($(GO) list -m -f '{{.Dir}}' $(LIBTORRENT_GO))"
107 |
108 | USERGRP = "$(shell id -u):$(shell id -g)"
109 |
110 | .PHONY: $(PLATFORMS)
111 |
112 | all:
113 | for i in $(PLATFORMS); do \
114 | $(MAKE) $$i; \
115 | done
116 |
117 | $(PLATFORMS):
118 | $(MAKE) build TARGET_OS=$(firstword $(subst -, ,$@)) TARGET_ARCH=$(word 2, $(subst -, ,$@))
119 |
120 | force:
121 | @true
122 |
123 | dependencies:
124 | $(GO) mod download
125 | chmod -R 755 $(LIBTORRENT_GO_HOME)
126 |
127 | libtorrent-go: dependencies force
128 | $(MAKE) -C $(LIBTORRENT_GO_HOME) $(PLATFORM)
129 |
130 | libtorrent-go-debug: dependencies force
131 | $(MAKE) -C $(LIBTORRENT_GO_HOME) debug PLATFORM=$(PLATFORM)
132 |
133 | libtorrent-go-defines: dependencies force
134 | $(MAKE) -C $(LIBTORRENT_GO_HOME) defines
135 |
136 | $(BUILD_PATH):
137 | mkdir -p $(BUILD_PATH)
138 |
139 | $(BUILD_PATH)/$(OUTPUT_NAME): libtorrent-go-defines $(BUILD_PATH) force
140 | export LDFLAGS='$(LDFLAGS)'; \
141 | export CC='$(CC)'; \
142 | export CXX='$(CXX)'; \
143 | export GOOS='$(GOOS)'; \
144 | export GOARCH='$(GOARCH)'; \
145 | export GOARM='$(GOARM)'; \
146 | export CGO_ENABLED='$(CGO_ENABLED)'; \
147 | $(GO) build -v \
148 | -gcflags '$(GO_GCFLAGS)' \
149 | -ldflags '$(GO_LDFLAGS)' \
150 | -o '$(BUILD_PATH)/$(OUTPUT_NAME)' \
151 | $(PKGDIR) && \
152 | set -x && \
153 | $(GO) vet -unsafeptr=false .
154 |
155 | vendor_darwin vendor_linux vendor_windows:
156 |
157 | vendor_android:
158 | cp $(CROSS_ROOT)/sysroot/usr/lib/$(CROSS_TRIPLE)/libc++_shared.so $(BUILD_PATH)
159 | chmod +rx $(BUILD_PATH)/libc++_shared.so
160 |
161 | torrest: $(BUILD_PATH)/$(OUTPUT_NAME)
162 |
163 | re: clean build
164 |
165 | clean:
166 | rm -rf $(BUILD_PATH)
167 |
168 | distclean:
169 | rm -rf $(BUILD_DIR)
170 |
171 | build: force
172 | $(DOCKER) run --rm \
173 | -u $(USERGRP) \
174 | -e GOPATH=$(DOCKER_GOPATH) \
175 | -e GOCACHE=$(DOCKER_GOCACHE) \
176 | -v "$(GOPATH)":$(DOCKER_GOPATH) \
177 | -v "$(WORKDIR)":$(DOCKER_WORKDIR) \
178 | -w $(DOCKER_WORKDIR) \
179 | $(PROJECT)/libtorrent-go-$(TARGET_OS)-$(TARGET_ARCH):$(LIBTORRENT_TAG) \
180 | make dist TARGET_OS=$(TARGET_OS) TARGET_ARCH=$(TARGET_ARCH) GIT_VERSION=$(GIT_VERSION)
181 |
182 | docker: force
183 | $(DOCKER) run --rm -it \
184 | -e GOPATH=$(DOCKER_GOPATH) \
185 | -v "$(GOPATH)":$(DOCKER_GOPATH) \
186 | -v "$(WORKDIR)":$(DOCKER_WORKDIR) \
187 | -w $(DOCKER_WORKDIR) \
188 | $(PROJECT)/libtorrent-go-$(TARGET_OS)-$(TARGET_ARCH):$(LIBTORRENT_TAG) bash
189 |
190 | strip: force
191 | @find $(BUILD_PATH) -type f ! -name "*.exe" -exec $(STRIP) {} \;
192 |
193 | upx: force
194 | # Do not .exe files, as upx doesn't really work with 8l/6l linked files.
195 | # It's fine for other platforms, because we link with an external linker, namely
196 | # GCC or Clang. However, on Windows this feature is not yet supported.
197 | @find $(BUILD_PATH) -type f ! -name "*.exe" -a ! -name "*.so" -exec $(UPX) --lzma {} \;
198 |
199 | checksum: $(BUILD_PATH)/$(OUTPUT_NAME)
200 | shasum -b $(BUILD_PATH)/$(OUTPUT_NAME) | cut -d' ' -f1 >> $(BUILD_PATH)/$(OUTPUT_NAME)
201 |
202 | dist: torrest vendor_$(TARGET_OS) strip checksum
203 |
204 | pull-all:
205 | for i in $(PLATFORMS); do \
206 | $(MAKE) pull PLATFORM=$$i; \
207 | done
208 |
209 | pull:
210 | $(DOCKER) pull $(PROJECT)/libtorrent-go-$(PLATFORM):$(LIBTORRENT_TAG)
211 |
212 | binaries:
213 | @set -e; \
214 | for platform in $(PLATFORMS); do \
215 | $(MAKE) zip PLATFORM=$${platform}; \
216 | done
217 |
218 | zip:
219 | cd $(BUILD_DIR) && mkdir -p binaries && \
220 | arch=$$(echo $(PLATFORM) | sed s/-/_/g) && \
221 | cd $${arch} && zip -9 -r ../binaries/$(NAME).$(GIT_VERSION).$${arch}.zip .
222 |
223 | # go get -u github.com/swaggo/swag/cmd/swag
224 | swag:
225 | swag init --generalInfo ./api/routes.go --output ./docs
226 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Torrest
2 |
3 | [](https://github.com/i96751414/torrest/actions?query=workflow%3Abuild)
4 | [](https://www.codacy.com/gh/i96751414/torrest/dashboard?utm_source=github.com&utm_medium=referral&utm_content=i96751414/torrest&utm_campaign=Badge_Grade)
5 |
6 | Torrent service with a REST api, specially made for streaming.
7 |
8 | > :warning: **This project will soon be deprecated in favor of [torrest-cpp](https://github.com/i96751414/torrest-cpp)**
9 |
10 | ## Building
11 | 1. Build the [cross-compiler](https://github.com/i96751414/cross-compiler) and [libtorrent-go](https://github.com/i96751414/libtorrent-go) images, or alternatively, pull the libtorrent-go images from [Docker Hub](https://hub.docker.com/r/i96751414/libtorrent-go):
12 |
13 | ```shell script
14 | make pull-all
15 | ```
16 | This will pull all platforms images. For a specific platform, run:
17 | ```shell script
18 | make pull PLATFORM=linux-x64
19 | ```
20 |
21 | 2. Build torrest binaries:
22 |
23 | ```shell script
24 | make all
25 | ```
26 | Or if you want to build for a specific platform:
27 | ```shell script
28 | make linux-x64
29 | ```
30 |
31 | The list of supported platforms is:
32 |
33 | |Android|Darwin|Linux|Windows|
34 | |-------|------|-----|-------|
35 | |android-arm
android-arm64
android-x64
android-x86
|darwin-x64
|linux-arm
linux-armv7
linux-arm64
linux-x64
linux-x86|windows-x64
windows-x86
|
36 |
37 | ### Swagger
38 | For building swagger docs, you must run `go get -u github.com/swaggo/swag/cmd/swag` to install all the necessary dependencies, and then run `make swag`.
39 | The last command must be executed before building the binaries, so the documents are included when building.
40 |
41 | Swagger-ui will then be available on: http://localhost:8080/swagger/index.html.
42 |
--------------------------------------------------------------------------------
/api/files.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/i96751414/torrest/util"
5 | "net/http"
6 | "strconv"
7 | "time"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/i96751414/torrest/bittorrent"
11 | "github.com/i96751414/torrest/settings"
12 | )
13 |
14 | type FileHash struct {
15 | Hash string `json:"hash"`
16 | }
17 |
18 | // @Summary Download File
19 | // @Description download file from torrent given its id
20 | // @ID download-file
21 | // @Produce json
22 | // @Param infoHash path string true "torrent info hash"
23 | // @Param file path integer true "file id"
24 | // @Param buffer query boolean false "buffer file"
25 | // @Success 200 {object} MessageResponse
26 | // @Failure 400 {object} ErrorResponse
27 | // @Failure 404 {object} ErrorResponse
28 | // @Router /torrents/{infoHash}/files/{file}/download [get]
29 | func downloadFile(config *settings.Settings, service *bittorrent.Service) gin.HandlerFunc {
30 | return func(ctx *gin.Context) {
31 | onGetFile(ctx, service, func(file *bittorrent.File) {
32 | file.SetPriority(bittorrent.DefaultPriority)
33 | if ctx.DefaultQuery("buffer", "false") == "true" {
34 | bufferSize := int64(float64(file.Length()) * startBufferPercent)
35 | if bufferSize < config.BufferSize {
36 | bufferSize = config.BufferSize
37 | }
38 | file.Buffer(bufferSize, endBufferSize)
39 | }
40 | ctx.JSON(http.StatusOK, NewMessageResponse("file '%d' is downloading", file.Id()))
41 | })
42 | }
43 | }
44 |
45 | // @Summary Stop File Download
46 | // @Description stop file download from torrent given its id
47 | // @ID stop-file
48 | // @Produce json
49 | // @Param infoHash path string true "torrent info hash"
50 | // @Param file path integer true "file id"
51 | // @Success 200 {object} MessageResponse
52 | // @Failure 400 {object} ErrorResponse
53 | // @Failure 404 {object} ErrorResponse
54 | // @Router /torrents/{infoHash}/files/{file}/stop [get]
55 | func stopFile(service *bittorrent.Service) gin.HandlerFunc {
56 | return func(ctx *gin.Context) {
57 | onGetFile(ctx, service, func(file *bittorrent.File) {
58 | file.SetPriority(bittorrent.DontDownloadPriority)
59 | ctx.JSON(http.StatusOK, NewMessageResponse("stopped file '%d' download", file.Id()))
60 | })
61 | }
62 | }
63 |
64 | // @Summary Get File Info
65 | // @Description get file info from torrent given its id
66 | // @ID file-info
67 | // @Produce json
68 | // @Param infoHash path string true "torrent info hash"
69 | // @Param file path integer true "file id"
70 | // @Success 200 {object} bittorrent.FileInfo
71 | // @Failure 400 {object} ErrorResponse
72 | // @Failure 404 {object} ErrorResponse
73 | // @Router /torrents/{infoHash}/files/{file}/info [get]
74 | func fileInfo(service *bittorrent.Service) gin.HandlerFunc {
75 | return func(ctx *gin.Context) {
76 | onGetFile(ctx, service, func(file *bittorrent.File) {
77 | ctx.JSON(http.StatusOK, file.Info())
78 | })
79 | }
80 | }
81 |
82 | // @Summary Get File Status
83 | // @Description get file status from torrent given its id
84 | // @ID file-status
85 | // @Produce json
86 | // @Param infoHash path string true "torrent info hash"
87 | // @Param file path integer true "file id"
88 | // @Success 200 {object} bittorrent.FileStatus
89 | // @Failure 400 {object} ErrorResponse
90 | // @Failure 404 {object} ErrorResponse
91 | // @Router /torrents/{infoHash}/files/{file}/status [get]
92 | func fileStatus(service *bittorrent.Service) gin.HandlerFunc {
93 | return func(ctx *gin.Context) {
94 | onGetFile(ctx, service, func(file *bittorrent.File) {
95 | ctx.JSON(http.StatusOK, file.Status())
96 | })
97 | }
98 | }
99 |
100 | // @Summary Calculate file hash
101 | // @Description calculate file hash suitable for opensubtitles
102 | // @ID file-hash
103 | // @Produce json
104 | // @Param infoHash path string true "torrent info hash"
105 | // @Param file path integer true "file id"
106 | // @Success 200 {object} FileHash
107 | // @Failure 400 {object} ErrorResponse
108 | // @Failure 404 {object} ErrorResponse
109 | // @Failure 500 {object} ErrorResponse
110 | // @Router /torrents/{infoHash}/files/{file}/hash [get]
111 | func fileHash(service *bittorrent.Service) gin.HandlerFunc {
112 | return func(ctx *gin.Context) {
113 | onGetFile(ctx, service, func(file *bittorrent.File) {
114 | reader := file.NewReader()
115 | if hash, err := util.HashFile(reader, file.Length()); err == nil {
116 | ctx.JSON(http.StatusOK, FileHash{Hash: hash})
117 | } else {
118 | ctx.JSON(http.StatusInternalServerError, NewErrorResponse(err))
119 | }
120 | if err := reader.Close(); err != nil {
121 | log.Errorf("Error closing file reader: %s\n", err)
122 | }
123 | })
124 | }
125 | }
126 |
127 | // @Summary Serve File
128 | // @Description serve file from torrent given its id
129 | // @ID serve-file
130 | // @Produce json
131 | // @Param infoHash path string true "torrent info hash"
132 | // @Param file path integer true "file id"
133 | // @Success 200
134 | // @Failure 400 {object} ErrorResponse
135 | // @Failure 404 {object} ErrorResponse
136 | // @Router /torrents/{infoHash}/files/{file}/serve [get]
137 | func serveFile(service *bittorrent.Service) gin.HandlerFunc {
138 | return func(ctx *gin.Context) {
139 | onGetFile(ctx, service, func(file *bittorrent.File) {
140 | reader := file.NewReader()
141 | reader.RegisterCloseNotifier(ctx.Writer.CloseNotify())
142 | http.ServeContent(ctx.Writer, ctx.Request, file.Name(), time.Time{}, reader)
143 | if err := reader.Close(); err != nil {
144 | log.Errorf("Error closing file reader: %s\n", err)
145 | }
146 | })
147 | }
148 | }
149 |
150 | // Can produce 400 (StatusBadRequest) and 404 (StatusNotFound) http errors
151 | func onGetFile(ctx *gin.Context, service *bittorrent.Service, f func(*bittorrent.File)) {
152 | fileString := ctx.Param("file")
153 | if fileId, err := strconv.Atoi(fileString); err == nil {
154 | onGetTorrent(ctx, service, func(torrent *bittorrent.Torrent) {
155 | if file, err := torrent.GetFile(fileId); err == nil {
156 | f(file)
157 | } else {
158 | ctx.JSON(http.StatusBadRequest, NewErrorResponse(err))
159 | }
160 | })
161 | } else {
162 | ctx.JSON(http.StatusBadRequest, NewErrorResponse("'file' must be integer"))
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/api/routes.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "fmt"
5 | "github.com/gin-gonic/gin"
6 | "github.com/i96751414/torrest/bittorrent"
7 | _ "github.com/i96751414/torrest/docs"
8 | "github.com/i96751414/torrest/settings"
9 | "github.com/op/go-logging"
10 | swaggerFiles "github.com/swaggo/files"
11 | "github.com/swaggo/gin-swagger"
12 | "time"
13 | )
14 |
15 | var log = logging.MustGetLogger("api")
16 |
17 | type ErrorResponse struct {
18 | Error string `json:"error" example:"Houston, we have a problem!"`
19 | }
20 |
21 | type MessageResponse struct {
22 | Message string `json:"message" example:"done"`
23 | }
24 |
25 | func NewErrorResponse(err interface{}) *ErrorResponse {
26 | r := ErrorResponse{}
27 | switch err.(type) {
28 | case string:
29 | r.Error = err.(string)
30 | case error:
31 | r.Error = err.(error).Error()
32 | default:
33 | panic("expecting either string or error")
34 | }
35 | return &r
36 | }
37 |
38 | func NewMessageResponse(format string, a ...interface{}) *MessageResponse {
39 | return &MessageResponse{Message: fmt.Sprintf(format, a...)}
40 | }
41 |
42 | // @title Torrest API
43 | // @version 1.0
44 | // @description Torrent server with a REST API
45 |
46 | // @contact.name i96751414
47 | // @contact.url https://github.com/i96751414/torrest
48 | // @contact.email i96751414@gmail.com
49 |
50 | // @license.name MIT
51 | // @license.url https://github.com/i96751414/torrest/blob/master/LICENSE
52 |
53 | // @BasePath /
54 |
55 | // Routes defines all the routes of the server
56 | func Routes(config *settings.Settings, service *bittorrent.Service, origin string) *gin.Engine {
57 | setLogLevel(config)
58 | gin.SetMode(gin.ReleaseMode)
59 |
60 | r := gin.New()
61 | // We might be suppressing bittorrent panics with gin.Recovery()
62 | r.Use(Logger(log), CORSMiddleware(origin), gin.Recovery())
63 |
64 | r.GET("/status", status(service))
65 | r.GET("/pause", pause(service))
66 | r.GET("/resume", resume(service))
67 |
68 | addRoute := r.Group("/add")
69 | addRoute.GET("/magnet", addMagnet(service))
70 | addRoute.POST("/torrent", addTorrent(service))
71 |
72 | settingsRoutes := r.Group("/settings")
73 | settingsRoutes.GET("/get", getSettings(config))
74 | settingsRoutes.POST("/set", setSettings(config, service))
75 |
76 | torrentsRoutes := r.Group("/torrents")
77 | torrentsRoutes.GET("/", listTorrents(service))
78 | torrentsRoutes.GET("/:infoHash/pause", pauseTorrent(service))
79 | torrentsRoutes.GET("/:infoHash/resume", resumeTorrent(service))
80 | torrentsRoutes.GET("/:infoHash/remove", removeTorrent(service))
81 | torrentsRoutes.GET("/:infoHash/info", torrentInfo(service))
82 | torrentsRoutes.GET("/:infoHash/status", torrentStatus(service))
83 | torrentsRoutes.GET("/:infoHash/files", torrentFiles(service))
84 | torrentsRoutes.GET("/:infoHash/download", downloadTorrent(service))
85 | torrentsRoutes.GET("/:infoHash/stop", stopTorrent(service))
86 | torrentsRoutes.GET("/:infoHash/files/:file/download", downloadFile(config, service))
87 | torrentsRoutes.GET("/:infoHash/files/:file/stop", stopFile(service))
88 | torrentsRoutes.GET("/:infoHash/files/:file/info", fileInfo(service))
89 | torrentsRoutes.GET("/:infoHash/files/:file/status", fileStatus(service))
90 | torrentsRoutes.GET("/:infoHash/files/:file/hash", fileHash(service))
91 | torrentsRoutes.Any("/:infoHash/files/:file/serve", serveFile(service))
92 |
93 | r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler,
94 | ginSwagger.URL("/swagger/doc.json")))
95 |
96 | return r
97 | }
98 |
99 | func setLogLevel(config *settings.Settings) {
100 | logging.SetLevel(config.ApiLogLevel, log.Module)
101 | }
102 |
103 | func CORSMiddleware(origin string) gin.HandlerFunc {
104 | return func(c *gin.Context) {
105 | c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
106 | c.Next()
107 | }
108 | }
109 |
110 | func Logger(logger *logging.Logger) gin.HandlerFunc {
111 | return func(c *gin.Context) {
112 | start := time.Now()
113 | path := c.Request.URL.Path
114 | raw := c.Request.URL.RawQuery
115 | if raw != "" {
116 | path = path + "?" + raw
117 | }
118 |
119 | c.Next()
120 |
121 | latency := time.Since(start)
122 | clientIP := c.ClientIP()
123 | statusCode := c.Writer.Status()
124 | method := c.Request.Method
125 | errorMessage := c.Errors.String()
126 |
127 | if errorMessage != "" {
128 | errorMessage = " :" + errorMessage
129 | }
130 |
131 | if latency > time.Minute {
132 | latency = latency - latency%time.Second
133 | }
134 |
135 | var logFunc func(string, ...interface{})
136 | if statusCode >= 500 {
137 | logFunc = logger.Errorf
138 | } else if statusCode >= 400 {
139 | logFunc = logger.Warningf
140 | } else {
141 | logFunc = logger.Infof
142 | }
143 |
144 | logFunc("GIN | %3d | %13v | %15s | %-7s %s%s", statusCode, latency, clientIP, method, path, errorMessage)
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/api/service.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "mime/multipart"
5 | "net/http"
6 | "strings"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/i96751414/torrest/bittorrent"
10 | )
11 |
12 | type NewTorrentResponse struct {
13 | InfoHash string `json:"info_hash" example:"000102030405060708090a0b0c0d0e0f10111213"`
14 | }
15 |
16 | // @Summary Status
17 | // @Description get service status
18 | // @ID status
19 | // @Produce json
20 | // @Success 200 {object} bittorrent.ServiceStatus
21 | // @Router /status [get]
22 | func status(service *bittorrent.Service) gin.HandlerFunc {
23 | return func(ctx *gin.Context) {
24 | ctx.JSON(http.StatusOK, service.GetStatus())
25 | }
26 | }
27 |
28 | // @Summary Pause
29 | // @Description pause service
30 | // @ID pause
31 | // @Produce json
32 | // @Success 200 {object} MessageResponse
33 | // @Router /pause [get]
34 | func pause(service *bittorrent.Service) gin.HandlerFunc {
35 | return func(ctx *gin.Context) {
36 | service.Pause()
37 | ctx.JSON(http.StatusOK, NewMessageResponse("service paused"))
38 | }
39 | }
40 |
41 | // @Summary Resume
42 | // @Description resume service
43 | // @ID resume
44 | // @Produce json
45 | // @Success 200 {object} MessageResponse
46 | // @Router /resume [get]
47 | func resume(service *bittorrent.Service) gin.HandlerFunc {
48 | return func(ctx *gin.Context) {
49 | service.Resume()
50 | ctx.JSON(http.StatusOK, NewMessageResponse("service resumed"))
51 | }
52 | }
53 |
54 | // @Summary Add Magnet
55 | // @Description add magnet to service
56 | // @ID add-magnet
57 | // @Produce json
58 | // @Param uri query string true "magnet URI"
59 | // @Param ignore_duplicate query boolean false "ignore if duplicate"
60 | // @Param download query boolean false "start downloading"
61 | // @Success 200 {object} NewTorrentResponse
62 | // @Failure 400 {object} ErrorResponse
63 | // @Failure 500 {object} ErrorResponse
64 | // @Router /add/magnet [get]
65 | func addMagnet(service *bittorrent.Service) gin.HandlerFunc {
66 | return func(ctx *gin.Context) {
67 | magnet := ctx.Query("uri")
68 | if !strings.HasPrefix(magnet, "magnet:") {
69 | ctx.JSON(http.StatusBadRequest, NewErrorResponse("Invalid magnet provided"))
70 | return
71 | }
72 | download := ctx.DefaultQuery("download", "false") == "true"
73 | if infoHash, err := service.AddMagnet(magnet, download); err == nil ||
74 | (err == bittorrent.DuplicateTorrentError &&
75 | ctx.DefaultQuery("ignore_duplicate", "false") == "true") {
76 | ctx.JSON(http.StatusOK, NewTorrentResponse{InfoHash: infoHash})
77 | } else {
78 | ctx.JSON(http.StatusInternalServerError, NewErrorResponse(err))
79 | }
80 | }
81 | }
82 |
83 | // @Summary Add Torrent File
84 | // @Description add torrent file to service
85 | // @ID add-torrent
86 | // @Accept multipart/form-data
87 | // @Produce json
88 | // @Param torrent formData file true "torrent file"
89 | // @Param ignore_duplicate query boolean false "ignore if duplicate"
90 | // @Param download query boolean false "start downloading"
91 | // @Success 200 {object} NewTorrentResponse
92 | // @Failure 400 {object} ErrorResponse
93 | // @Failure 500 {object} ErrorResponse
94 | // @Router /add/torrent [post]
95 | func addTorrent(service *bittorrent.Service) gin.HandlerFunc {
96 | return func(ctx *gin.Context) {
97 | if f, err := ctx.FormFile("torrent"); err == nil {
98 | var err error
99 | var infoHash string
100 | var file multipart.File
101 |
102 | if file, err = f.Open(); err == nil {
103 | data := make([]byte, f.Size)
104 | if _, err = file.Read(data); err == nil {
105 | download := ctx.DefaultQuery("download", "false") == "true"
106 | if infoHash, err = service.AddTorrentData(data, download); err == nil ||
107 | (err == bittorrent.DuplicateTorrentError &&
108 | ctx.DefaultQuery("ignore_duplicate", "false") == "true") {
109 | ctx.JSON(http.StatusOK, NewTorrentResponse{InfoHash: infoHash})
110 | return
111 | }
112 | }
113 | }
114 |
115 | ctx.JSON(http.StatusInternalServerError, NewErrorResponse(err))
116 | } else {
117 | ctx.JSON(http.StatusBadRequest, NewErrorResponse(err))
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/api/settings.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/i96751414/torrest/bittorrent"
5 | "io/ioutil"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/i96751414/torrest/settings"
10 | )
11 |
12 | // @Summary Get current settings
13 | // @Description get settings in JSON object
14 | // @ID get-settings
15 | // @Produce json
16 | // @Success 200 {object} settings.Settings
17 | // @Router /settings/get [get]
18 | func getSettings(config *settings.Settings) gin.HandlerFunc {
19 | return func(ctx *gin.Context) {
20 | ctx.JSON(http.StatusOK, config)
21 | }
22 | }
23 |
24 | // @Summary Set settings
25 | // @Description set settings given the provided JSON object
26 | // @ID set-settings
27 | // @Accept json
28 | // @Produce json
29 | // @Param default body settings.Settings false "Settings to be set"
30 | // @Param reset query boolean false "reset torrents"
31 | // @Success 200 {object} settings.Settings
32 | // @Failure 500 {object} ErrorResponse
33 | // @Router /settings/set [post]
34 | func setSettings(config *settings.Settings, service *bittorrent.Service) gin.HandlerFunc {
35 | return func(ctx *gin.Context) {
36 | body, err := ioutil.ReadAll(ctx.Request.Body)
37 | if err != nil {
38 | ctx.JSON(http.StatusInternalServerError, NewErrorResponse(err))
39 | return
40 | }
41 |
42 | newConfig := config.Clone()
43 | if err := newConfig.Update(body); err != nil {
44 | ctx.JSON(http.StatusInternalServerError, NewErrorResponse(err))
45 | return
46 | }
47 |
48 | setLogLevel(newConfig)
49 | reset := ctx.DefaultQuery("reset", "false") == "true"
50 | service.Reconfigure(newConfig, reset)
51 |
52 | if err := newConfig.Save(); err != nil {
53 | log.Errorf("Failed saving settings: %s", err)
54 | }
55 | if err := config.UpdateFrom(newConfig); err != nil {
56 | log.Errorf("Failed updating global settings: %s", err)
57 | }
58 |
59 | ctx.JSON(http.StatusOK, newConfig)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/api/torrents.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/i96751414/torrest/bittorrent"
8 | )
9 |
10 | const (
11 | startBufferPercent = 0.005
12 | endBufferSize = 10 * 1024 * 1024 // 10MB
13 | )
14 |
15 | type FileInfoResponse struct {
16 | *bittorrent.FileInfo
17 | Status *bittorrent.FileStatus `json:"status,omitempty"`
18 | }
19 |
20 | type TorrentInfoResponse struct {
21 | *bittorrent.TorrentInfo
22 | Status *bittorrent.TorrentStatus `json:"status,omitempty"`
23 | }
24 |
25 | // @Summary List Torrents
26 | // @Description list all torrents from service
27 | // @ID list-torrents
28 | // @Produce json
29 | // @Param status query boolean false "get torrents status"
30 | // @Success 200 {array} TorrentInfoResponse
31 | // @Router /torrents [get]
32 | func listTorrents(service *bittorrent.Service) gin.HandlerFunc {
33 | return func(ctx *gin.Context) {
34 | torrents := service.Torrents()
35 | response := make([]TorrentInfoResponse, len(torrents))
36 | for i, torrent := range torrents {
37 | response[i].TorrentInfo = torrent.GetInfo()
38 | }
39 | if ctx.DefaultQuery("status", "false") == "true" {
40 | for i, torrent := range torrents {
41 | response[i].Status = torrent.GetStatus()
42 | }
43 | }
44 | ctx.JSON(http.StatusOK, response)
45 | }
46 | }
47 |
48 | // @Summary Remove Torrent
49 | // @Description remove torrent from service
50 | // @ID remove-torrent
51 | // @Produce json
52 | // @Param infoHash path string true "torrent info hash"
53 | // @Param delete query boolean false "delete files"
54 | // @Success 200 {object} MessageResponse
55 | // @Failure 404 {object} ErrorResponse
56 | // @Router /torrents/{infoHash}/remove [get]
57 | func removeTorrent(service *bittorrent.Service) gin.HandlerFunc {
58 | return func(ctx *gin.Context) {
59 | infoHash := ctx.Param("infoHash")
60 | removeFiles := ctx.DefaultQuery("delete", "true") != "false"
61 |
62 | if err := service.RemoveTorrent(infoHash, removeFiles); err == nil {
63 | ctx.JSON(http.StatusOK, NewMessageResponse("Torrent '%s' deleted", infoHash))
64 | } else {
65 | ctx.JSON(http.StatusNotFound, NewErrorResponse(err))
66 | }
67 | }
68 | }
69 |
70 | // @Summary Resume Torrent
71 | // @Description resume a paused torrent
72 | // @ID resume-torrent
73 | // @Produce json
74 | // @Param infoHash path string true "torrent info hash"
75 | // @Success 200 {object} MessageResponse
76 | // @Failure 404 {object} ErrorResponse
77 | // @Router /torrents/{infoHash}/resume [get]
78 | func resumeTorrent(service *bittorrent.Service) gin.HandlerFunc {
79 | return func(ctx *gin.Context) {
80 | onGetTorrent(ctx, service, func(torrent *bittorrent.Torrent) {
81 | torrent.Resume()
82 | ctx.JSON(http.StatusOK, NewMessageResponse("Torrent '%s' resumed", torrent.InfoHash()))
83 | })
84 | }
85 | }
86 |
87 | // @Summary Pause Torrent
88 | // @Description pause torrent from service
89 | // @ID pause-torrent
90 | // @Produce json
91 | // @Param infoHash path string true "torrent info hash"
92 | // @Success 200 {object} MessageResponse
93 | // @Failure 404 {object} ErrorResponse
94 | // @Router /torrents/{infoHash}/pause [get]
95 | func pauseTorrent(service *bittorrent.Service) gin.HandlerFunc {
96 | return func(ctx *gin.Context) {
97 | onGetTorrent(ctx, service, func(torrent *bittorrent.Torrent) {
98 | torrent.Pause()
99 | ctx.JSON(http.StatusOK, NewMessageResponse("Torrent '%s' paused", torrent.InfoHash()))
100 | })
101 | }
102 | }
103 |
104 | // @Summary Get Torrent Info
105 | // @Description get torrent info
106 | // @ID torrent-info
107 | // @Produce json
108 | // @Param infoHash path string true "torrent info hash"
109 | // @Success 200 {object} bittorrent.TorrentInfo
110 | // @Failure 404 {object} ErrorResponse
111 | // @Router /torrents/{infoHash}/info [get]
112 | func torrentInfo(service *bittorrent.Service) gin.HandlerFunc {
113 | return func(ctx *gin.Context) {
114 | onGetTorrent(ctx, service, func(torrent *bittorrent.Torrent) {
115 | ctx.JSON(http.StatusOK, torrent.GetInfo())
116 | })
117 | }
118 | }
119 |
120 | // @Summary Get Torrent Status
121 | // @Description get torrent status
122 | // @ID torrent-status
123 | // @Produce json
124 | // @Param infoHash path string true "torrent info hash"
125 | // @Success 200 {object} bittorrent.TorrentStatus
126 | // @Failure 404 {object} ErrorResponse
127 | // @Router /torrents/{infoHash}/status [get]
128 | func torrentStatus(service *bittorrent.Service) gin.HandlerFunc {
129 | return func(ctx *gin.Context) {
130 | onGetTorrent(ctx, service, func(torrent *bittorrent.Torrent) {
131 | ctx.JSON(http.StatusOK, torrent.GetStatus())
132 | })
133 | }
134 | }
135 |
136 | // @Summary Get Torrent Files
137 | // @Description get a list of the torrent files and its details
138 | // @ID torrent-files
139 | // @Produce json
140 | // @Param infoHash path string true "torrent info hash"
141 | // @Param status query boolean false "get files status"
142 | // @Success 200 {array} FileInfoResponse
143 | // @Failure 404 {object} ErrorResponse
144 | // @Failure 500 {object} ErrorResponse
145 | // @Router /torrents/{infoHash}/files [get]
146 | func torrentFiles(service *bittorrent.Service) gin.HandlerFunc {
147 | return func(ctx *gin.Context) {
148 | onGetTorrent(ctx, service, func(torrent *bittorrent.Torrent) {
149 | if files, err := torrent.Files(); err == nil {
150 | response := make([]FileInfoResponse, len(files))
151 | for i, file := range files {
152 | response[i].FileInfo = file.Info()
153 | }
154 | if ctx.DefaultQuery("status", "false") == "true" {
155 | for i, file := range files {
156 | response[i].Status = file.Status()
157 | }
158 | }
159 | ctx.JSON(http.StatusOK, response)
160 | } else {
161 | ctx.JSON(http.StatusInternalServerError, NewErrorResponse(err))
162 | }
163 | })
164 | }
165 | }
166 |
167 | // @Summary Download
168 | // @Description download all files from torrent
169 | // @ID download-torrent
170 | // @Produce json
171 | // @Param infoHash path string true "torrent info hash"
172 | // @Success 200 {object} MessageResponse
173 | // @Failure 404 {object} ErrorResponse
174 | // @Failure 500 {object} ErrorResponse
175 | // @Router /torrents/{infoHash}/download [get]
176 | func downloadTorrent(service *bittorrent.Service) gin.HandlerFunc {
177 | return func(ctx *gin.Context) {
178 | onGetTorrent(ctx, service, func(torrent *bittorrent.Torrent) {
179 | if err := torrent.SetPriority(bittorrent.DefaultPriority); err == nil {
180 | ctx.JSON(http.StatusOK, NewMessageResponse("torrent '%s' is downloading", torrent.InfoHash()))
181 | } else {
182 | ctx.JSON(http.StatusInternalServerError, NewErrorResponse(err))
183 | }
184 | })
185 | }
186 | }
187 |
188 | // @Summary Stop Download
189 | // @Description stop downloading torrent
190 | // @ID stop-torrent
191 | // @Produce json
192 | // @Param infoHash path string true "torrent info hash"
193 | // @Success 200 {object} MessageResponse
194 | // @Failure 404 {object} ErrorResponse
195 | // @Failure 500 {object} ErrorResponse
196 | // @Router /torrents/{infoHash}/stop [get]
197 | func stopTorrent(service *bittorrent.Service) gin.HandlerFunc {
198 | return func(ctx *gin.Context) {
199 | onGetTorrent(ctx, service, func(torrent *bittorrent.Torrent) {
200 | if err := torrent.SetPriority(bittorrent.DontDownloadPriority); err == nil {
201 | ctx.JSON(http.StatusOK, NewMessageResponse("stopped torrent '%s' download", torrent.InfoHash()))
202 | } else {
203 | ctx.JSON(http.StatusInternalServerError, NewErrorResponse(err))
204 | }
205 | })
206 | }
207 | }
208 |
209 | // Can produce 404 (StatusNotFound) http error
210 | func onGetTorrent(ctx *gin.Context, service *bittorrent.Service, f func(*bittorrent.Torrent)) {
211 | infoHash := ctx.Param("infoHash")
212 | if torrent, err := service.GetTorrent(infoHash); err == nil {
213 | f(torrent)
214 | } else {
215 | ctx.JSON(http.StatusNotFound, NewErrorResponse(err))
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/bittorrent/errors.go:
--------------------------------------------------------------------------------
1 | package bittorrent
2 |
3 | import "errors"
4 |
5 | var (
6 | DuplicateTorrentError = errors.New("torrent was previously added")
7 | LoadTorrentError = errors.New("failed loading torrent")
8 | InvalidInfoHashError = errors.New("no such info hash")
9 | InvalidFileIdError = errors.New("no such file id")
10 | ServiceClosedError = errors.New("service was closed")
11 | TorrentClosedError = errors.New("torrent was closed")
12 | TorrentPausedError = errors.New("torrent paused")
13 | ReaderClosedError = errors.New("reader was closed")
14 | ReaderCloseNotifyError = errors.New("reader close notify received")
15 | InvalidWhenceError = errors.New("invalid whence")
16 | TimeoutError = errors.New("timeout reached")
17 | NoMetadataError = errors.New("no metadata")
18 | )
19 |
--------------------------------------------------------------------------------
/bittorrent/file.go:
--------------------------------------------------------------------------------
1 | package bittorrent
2 |
3 | import (
4 | "path/filepath"
5 | "sync"
6 |
7 | "github.com/i96751414/libtorrent-go"
8 | )
9 |
10 | type File struct {
11 | mu *sync.RWMutex
12 | torrent *Torrent
13 | index int
14 | offset int64
15 | length int64
16 | path string
17 | name string
18 | pieceLength int64
19 | bufferPieces []int
20 | bufferSize int64
21 | priority uint
22 | isBuffering bool
23 | }
24 |
25 | type FileInfo struct {
26 | Id int `json:"id"`
27 | Length int64 `json:"length"`
28 | Path string `json:"path"`
29 | Name string `json:"name"`
30 | }
31 |
32 | type FileStatus struct {
33 | Total int64 `json:"total"`
34 | TotalDone int64 `json:"total_done"`
35 | Progress float64 `json:"progress"`
36 | Priority uint `json:"priority"`
37 | BufferingTotal int64 `json:"buffering_total"`
38 | BufferingProgress float64 `json:"buffering_progress"`
39 | State LTStatus `json:"state"`
40 | }
41 |
42 | func NewFile(torrent *Torrent, storage libtorrent.FileStorage, index int) *File {
43 | f := &File{
44 | mu: &sync.RWMutex{},
45 | torrent: torrent,
46 | index: index,
47 | offset: storage.FileOffset(index),
48 | length: storage.FileSize(index),
49 | path: storage.FilePath(index),
50 | name: storage.FileName(index),
51 | pieceLength: int64(storage.PieceLength()),
52 | priority: torrent.handle.FilePriority(index).(uint),
53 | }
54 |
55 | if f.priority == DontDownloadPriority {
56 | // Make sure we don't have individual pieces downloading
57 | // previously set by Buffer
58 | f.SetPriority(DontDownloadPriority)
59 | }
60 |
61 | return f
62 | }
63 |
64 | func (f *File) Info() *FileInfo {
65 | return &FileInfo{
66 | Id: f.index,
67 | Length: f.length,
68 | Path: f.path,
69 | Name: f.name,
70 | }
71 | }
72 |
73 | func (f *File) Status() *FileStatus {
74 | f.mu.RLock()
75 | defer f.mu.RUnlock()
76 | return &FileStatus{
77 | Total: f.length,
78 | TotalDone: f.BytesCompleted(),
79 | Progress: f.GetProgress(),
80 | Priority: f.priority,
81 | BufferingTotal: f.bufferSize,
82 | BufferingProgress: f.getBufferingProgress(),
83 | State: f.GetState(),
84 | }
85 | }
86 |
87 | func (f *File) Id() int {
88 | return f.index
89 | }
90 |
91 | func (f *File) Length() int64 {
92 | return f.length
93 | }
94 |
95 | func (f *File) Path() string {
96 | return f.path
97 | }
98 |
99 | func (f *File) Name() string {
100 | return f.name
101 | }
102 |
103 | func (f *File) NewReader() *reader {
104 | return newReader(f.torrent, f.offset, f.length, f.pieceLength, 0.01)
105 | }
106 |
107 | func (f *File) GetDownloadPath() string {
108 | return filepath.Join(f.torrent.service.config.DownloadPath, f.path)
109 | }
110 |
111 | func (f *File) getPiecesIndexes(off, length int64) (firstPieceIndex, endPieceIndex int) {
112 | if off < 0 {
113 | off = 0
114 | }
115 | end := off + length
116 | if end > f.length {
117 | end = f.length
118 | }
119 | firstPieceIndex = int((f.offset + off) / f.pieceLength)
120 | endPieceIndex = int((f.offset + end - 1) / f.pieceLength)
121 | return
122 | }
123 |
124 | func (f *File) GetProgress() float64 {
125 | return 100 * float64(f.BytesCompleted()) / float64(f.length)
126 | }
127 |
128 | func (f *File) BytesCompleted() int64 {
129 | return f.torrent.getFilesDownloadedBytes()[f.index]
130 | }
131 |
132 | func (f *File) SetPriority(priority uint) {
133 | log.Debugf("Setting file %s:%d with priority %d", f.torrent.infoHash, f.index, priority)
134 | f.mu.Lock()
135 | defer f.mu.Unlock()
136 |
137 | f.priority = priority
138 | f.isBuffering = false
139 | f.bufferSize = 0
140 | f.bufferPieces = nil
141 | f.torrent.handle.FilePriority(f.index, priority)
142 | }
143 |
144 | func (f *File) IsDownloading() bool {
145 | f.mu.RLock()
146 | defer f.mu.RUnlock()
147 | return f.isBuffering || f.priority != DontDownloadPriority
148 | }
149 |
150 | func (f *File) verifyBufferingState() bool {
151 | isBuffering := false
152 |
153 | if f.isBuffering {
154 | f.mu.Lock()
155 | defer f.mu.Unlock()
156 | if f.isBuffering {
157 | if f.bufferBytesMissing() == 0 {
158 | f.isBuffering = false
159 | } else {
160 | isBuffering = true
161 | }
162 | }
163 | }
164 |
165 | return isBuffering
166 | }
167 |
168 | func (f *File) addBufferPiece(piece int, info libtorrent.TorrentInfo) {
169 | f.torrent.handle.PiecePriority(piece, TopPriority)
170 | f.torrent.handle.SetPieceDeadline(piece, 0)
171 | f.bufferSize += int64(info.PieceSize(piece))
172 | f.bufferPieces = append(f.bufferPieces, piece)
173 | }
174 |
175 | func (f *File) Buffer(startBufferSize, endBufferSize int64) {
176 | log.Debugf("Buffering file %s:%d", f.torrent.infoHash, f.index)
177 | f.mu.Lock()
178 | defer f.mu.Unlock()
179 |
180 | f.bufferSize = 0
181 | f.bufferPieces = nil
182 | info := f.torrent.handle.TorrentFile()
183 |
184 | if f.length >= startBufferSize+endBufferSize {
185 | aFirstPieceIndex, aEndPieceIndex := f.getPiecesIndexes(0, startBufferSize)
186 | for idx := aFirstPieceIndex; idx <= aEndPieceIndex; idx++ {
187 | f.addBufferPiece(idx, info)
188 | }
189 |
190 | bFirstPieceIndex, bEndPieceIndex := f.getPiecesIndexes(f.length-endBufferSize, endBufferSize)
191 | for idx := bFirstPieceIndex; idx <= bEndPieceIndex; idx++ {
192 | f.addBufferPiece(idx, info)
193 | }
194 | } else {
195 | firstPieceIndex, endPieceIndex := f.getPiecesIndexes(0, f.length)
196 | for idx := firstPieceIndex; idx <= endPieceIndex; idx++ {
197 | f.addBufferPiece(idx, info)
198 | }
199 | }
200 |
201 | f.isBuffering = true
202 | }
203 |
204 | func (f *File) bufferBytesMissing() int64 {
205 | return f.torrent.piecesBytesMissing(f.bufferPieces)
206 | }
207 |
208 | func (f *File) bufferBytesCompleted() int64 {
209 | return f.bufferSize - f.bufferBytesMissing()
210 | }
211 |
212 | func (f *File) getBufferingProgress() float64 {
213 | if f.bufferSize == 0 || !f.isBuffering {
214 | return 100
215 | }
216 | return float64(f.bufferBytesCompleted()) / float64(f.bufferSize) * 100.0
217 | }
218 |
219 | func (f *File) GetState() LTStatus {
220 | return f.torrent.getState(f)
221 | }
222 |
223 | func (f *File) GetBufferingProgress() float64 {
224 | f.mu.RLock()
225 | defer f.mu.RUnlock()
226 | return f.getBufferingProgress()
227 | }
228 |
229 | func (f *File) BufferLength() int64 {
230 | return f.bufferSize
231 | }
232 |
233 | func (f *File) BufferBytesMissing() int64 {
234 | f.mu.RLock()
235 | defer f.mu.RUnlock()
236 | return f.bufferBytesMissing()
237 | }
238 |
239 | func (f *File) BufferBytesCompleted() int64 {
240 | f.mu.RLock()
241 | defer f.mu.RUnlock()
242 | return f.bufferBytesCompleted()
243 | }
244 |
--------------------------------------------------------------------------------
/bittorrent/reader.go:
--------------------------------------------------------------------------------
1 | package bittorrent
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "sync"
7 | "time"
8 |
9 | "github.com/i96751414/libtorrent-go"
10 | )
11 |
12 | const (
13 | piecesRefreshDuration = 500 * time.Millisecond
14 | )
15 |
16 | type reader struct {
17 | mu *sync.Mutex
18 | storage libtorrent.StorageInterface
19 | torrent *Torrent
20 | offset int64
21 | length int64
22 | pieceLength int64
23 | priorityPieces int
24 | closing chan interface{}
25 | firstPiece int
26 | lastPiece int
27 | closeNotifiers []<-chan bool
28 | pos int64
29 | pieceWaitTimeout time.Duration
30 | }
31 |
32 | func newReader(torrent *Torrent, offset, length, pieceLength int64, readAhead float64) *reader {
33 | r := &reader{
34 | mu: &sync.Mutex{},
35 | storage: torrent.handle.GetStorageImpl(),
36 | torrent: torrent,
37 | offset: offset,
38 | length: length,
39 | pieceLength: pieceLength,
40 | priorityPieces: int(0.5 + readAhead*float64(length)/float64(pieceLength)),
41 | closing: make(chan interface{}),
42 | pieceWaitTimeout: torrent.service.config.PieceWaitTimeout * time.Second,
43 | }
44 | r.firstPiece = r.pieceFromOffset(0)
45 | r.lastPiece = r.pieceFromOffset(length - 1)
46 | return r
47 | }
48 |
49 | func (r *reader) RegisterCloseNotifier(n <-chan bool) {
50 | r.closeNotifiers = append(r.closeNotifiers, n)
51 | }
52 |
53 | func (r *reader) waitForPiece(piece int, timeout time.Duration) error {
54 | log.Debugf("Waiting for piece %d on '%s'", piece, r.torrent.infoHash)
55 |
56 | startTime := time.Now()
57 | pieceRefreshTicker := time.NewTicker(piecesRefreshDuration)
58 | defer pieceRefreshTicker.Stop()
59 |
60 | for !r.torrent.handle.HavePiece(piece) {
61 | select {
62 | case <-r.torrent.service.closing:
63 | return ServiceClosedError
64 | case <-r.torrent.closing:
65 | return TorrentClosedError
66 | case <-r.closing:
67 | return ReaderClosedError
68 | case <-pieceRefreshTicker.C:
69 | if timeout != 0 && time.Since(startTime) >= timeout {
70 | log.Warningf("Timed out waiting for piece %d with priority %v for '%s'",
71 | piece, r.torrent.handle.PiecePriority(piece), r.torrent.infoHash)
72 | return TimeoutError
73 | }
74 |
75 | for _, n := range r.closeNotifiers {
76 | select {
77 | case <-n:
78 | log.Debugf("Received close notify for '%s'", r.torrent.infoHash)
79 | return ReaderCloseNotifyError
80 | default:
81 | // do nothing
82 | }
83 | }
84 |
85 | if r.torrent.isPaused {
86 | log.Debugf("Torrent '%s' is paused", r.torrent.infoHash)
87 | return TorrentPausedError
88 | }
89 | }
90 | }
91 | return nil
92 | }
93 |
94 | func (r *reader) pieceFromOffset(offset int64) int {
95 | return int((r.offset + offset) / r.pieceLength)
96 | }
97 |
98 | func (r *reader) pieceOffsetFromOffset(offset int64) int64 {
99 | return (r.offset + offset) % r.pieceLength
100 | }
101 |
102 | func (r *reader) pieceOffset(piece int) int64 {
103 | return int64(piece)*r.pieceLength - r.offset
104 | }
105 |
106 | func (r *reader) Read(b []byte) (int, error) {
107 | r.mu.Lock()
108 | defer r.mu.Unlock()
109 |
110 | startPiece := r.pieceFromOffset(r.pos)
111 | endPiece := r.pieceFromOffset(r.pos + int64(len(b)) - 1)
112 | r.setPiecesPriorities(startPiece, endPiece-startPiece)
113 | for p := startPiece; p <= endPiece; p++ {
114 | if !r.torrent.handle.HavePiece(p) {
115 | if err := r.waitForPiece(p, r.pieceWaitTimeout); err != nil {
116 | return 0, err
117 | }
118 | }
119 | }
120 |
121 | storageError := libtorrent.NewStorageError()
122 | defer libtorrent.DeleteStorageError(storageError)
123 | n := r.storage.Read(b, int64(len(b)), startPiece, int(r.pieceOffsetFromOffset(r.pos)), storageError)
124 | if ec := storageError.GetEc(); ec.Failed() {
125 | message := ec.Message().(string)
126 | log.Errorf("Storage read error: %s", message)
127 | return n, errors.New(message)
128 | }
129 |
130 | r.pos += int64(n)
131 | return n, nil
132 | }
133 |
134 | func (r *reader) Close() error {
135 | log.Debugf("Closing reader for '%s'", r.torrent.infoHash)
136 | close(r.closing)
137 | return nil
138 | }
139 |
140 | func (r *reader) setPiecePriority(piece int, deadline int, priority uint) {
141 | if r.torrent.handle.PiecePriority(piece).(uint) < priority {
142 | r.torrent.handle.PiecePriority(piece, priority)
143 | r.torrent.handle.SetPieceDeadline(piece, deadline)
144 | }
145 | }
146 |
147 | func (r *reader) setPiecesPriorities(piece int, pieceEndOffset int) {
148 | endPiece := piece + pieceEndOffset + r.priorityPieces
149 | for p, i := piece, 0; p <= endPiece && p <= r.lastPiece; p, i = p+1, i+1 {
150 | if !r.torrent.handle.HavePiece(p) {
151 | if i <= pieceEndOffset {
152 | r.setPiecePriority(p, 0, TopPriority)
153 | } else {
154 | r.setPiecePriority(p, (i-pieceEndOffset)*10, HighPriority)
155 | }
156 | }
157 | }
158 | }
159 |
160 | func (r *reader) Seek(off int64, whence int) (int64, error) {
161 | r.mu.Lock()
162 | defer r.mu.Unlock()
163 |
164 | switch whence {
165 | case io.SeekStart:
166 | // do nothing
167 | case io.SeekCurrent:
168 | off += r.pos
169 | case io.SeekEnd:
170 | off += r.length
171 | default:
172 | off = -1
173 | }
174 |
175 | if off < 0 {
176 | return off, InvalidWhenceError
177 | }
178 |
179 | r.pos = off
180 | r.setPiecesPriorities(r.pieceFromOffset(off), 0)
181 | return off, nil
182 | }
183 |
--------------------------------------------------------------------------------
/bittorrent/service.go:
--------------------------------------------------------------------------------
1 | package bittorrent
2 |
3 | import (
4 | "encoding/hex"
5 | "errors"
6 | "io/ioutil"
7 | "os"
8 | "path/filepath"
9 | "regexp"
10 | "strconv"
11 | "strings"
12 | "sync"
13 | "time"
14 |
15 | "github.com/i96751414/libtorrent-go"
16 | "github.com/i96751414/torrest/settings"
17 | "github.com/i96751414/torrest/util"
18 | "github.com/op/go-logging"
19 | )
20 |
21 | var (
22 | log = logging.MustGetLogger("bittorrent")
23 | alertsLog = logging.MustGetLogger("alerts")
24 | portRegex = regexp.MustCompile(`:\d+$`)
25 | )
26 |
27 | var DefaultDhtBootstrapNodes = []string{
28 | "router.utorrent.com:6881",
29 | "router.bittorrent.com:6881",
30 | "dht.transmissionbt.com:6881",
31 | "dht.aelitis.com:6881", // Vuze
32 | "router.silotis.us:6881", // IPv6
33 | "dht.libtorrent.org:25401", // @arvidn's
34 | }
35 |
36 | const (
37 | libtorrentAlertWaitTime = time.Second
38 | libtorrentProgressTime = time.Second
39 | maxFilesPerTorrent = 1000
40 | )
41 |
42 | //noinspection GoUnusedConst
43 | const (
44 | ipToSDefault = iota
45 | ipToSLowDelay = 1 << iota
46 | ipToSReliability = 1 << iota
47 | ipToSThroughput = 1 << iota
48 | ipToSLowCost = 1 << iota
49 | )
50 |
51 | const (
52 | extTorrent = ".torrent"
53 | extMagnet = ".magnet"
54 | extParts = ".parts"
55 | extFastResume = ".fastresume"
56 | )
57 |
58 | // Service represents the torrent service
59 | type Service struct {
60 | session libtorrent.Session
61 | config *settings.Settings
62 | settingsPack libtorrent.SettingsPack
63 | torrents []*Torrent
64 | mu *sync.RWMutex
65 | wg *sync.WaitGroup
66 | rateLimited bool
67 | closing chan interface{}
68 | UserAgent string
69 | downloadRate int64
70 | uploadRate int64
71 | progress float64
72 | }
73 |
74 | type ServiceStatus struct {
75 | Progress float64 `json:"progress"`
76 | DownloadRate int64 `json:"download_rate"`
77 | UploadRate int64 `json:"upload_rate"`
78 | NumTorrents int `json:"num_torrents"`
79 | IsPaused bool `json:"is_paused"`
80 | }
81 |
82 | type Magnet struct {
83 | Uri string
84 | Download bool
85 | }
86 |
87 | // NewService creates a service given the provided configs
88 | func NewService(config *settings.Settings) *Service {
89 | createDir(config.DownloadPath)
90 | createDir(config.TorrentsPath)
91 |
92 | s := &Service{
93 | settingsPack: libtorrent.NewSettingsPack(),
94 | mu: &sync.RWMutex{},
95 | wg: &sync.WaitGroup{},
96 | rateLimited: true,
97 | closing: make(chan interface{}),
98 | }
99 |
100 | s.configure(config)
101 | s.loadTorrentFiles()
102 |
103 | s.wg.Add(3)
104 | go s.saveResumeDataLoop()
105 | go s.alertsConsumer()
106 | go s.downloadProgress()
107 |
108 | return s
109 | }
110 |
111 | func (s *Service) alertsConsumer() {
112 | defer s.wg.Done()
113 | ipRegex := regexp.MustCompile(`\.\d+`)
114 | for {
115 | select {
116 | case <-s.closing:
117 | return
118 | default:
119 | if s.session.WaitForAlert(libtorrentAlertWaitTime).Swigcptr() == 0 {
120 | continue
121 | }
122 |
123 | alerts := libtorrent.NewStdVectorAlerts()
124 | s.session.PopAlerts(alerts)
125 |
126 | for i := 0; i < int(alerts.Size()); i++ {
127 | ltAlert := alerts.Get(i)
128 | alertType := ltAlert.Type()
129 | alertPtr := ltAlert.Swigcptr()
130 | alertMessage := ltAlert.Message()
131 | category := ltAlert.Category()
132 | what := ltAlert.What()
133 |
134 | switch alertType {
135 | case libtorrent.SaveResumeDataAlertAlertType:
136 | s.onSaveResumeData(libtorrent.SwigcptrSaveResumeDataAlert(alertPtr))
137 |
138 | case libtorrent.ExternalIpAlertAlertType:
139 | alertMessage = ipRegex.ReplaceAllString(alertMessage, ".XX")
140 |
141 | case libtorrent.MetadataReceivedAlertAlertType:
142 | s.onMetadataReceived(libtorrent.SwigcptrMetadataReceivedAlert(alertPtr))
143 |
144 | case libtorrent.StateChangedAlertAlertType:
145 | s.onStateChanged(libtorrent.SwigcptrStateChangedAlert(alertPtr))
146 | }
147 |
148 | // log alerts
149 | var logFunc func(string, ...interface{})
150 | if category&libtorrent.AlertErrorNotification != 0 {
151 | logFunc = alertsLog.Errorf
152 | } else if category&libtorrent.AlertConnectNotification != 0 {
153 | logFunc = alertsLog.Debugf
154 | } else if category&libtorrent.AlertPerformanceWarning != 0 {
155 | logFunc = alertsLog.Warningf
156 | } else {
157 | logFunc = alertsLog.Noticef
158 | }
159 | logFunc("%s: %s", what, alertMessage)
160 | }
161 | libtorrent.DeleteStdVectorAlerts(alerts)
162 | }
163 | }
164 | }
165 |
166 | func (s *Service) onSaveResumeData(alert libtorrent.SaveResumeDataAlert) {
167 | torrentStatus := alert.GetHandle().Status(libtorrent.TorrentHandleQueryName)
168 | defer libtorrent.DeleteTorrentStatus(torrentStatus)
169 | infoHash := getInfoHash(torrentStatus.GetInfoHash())
170 |
171 | params := alert.GetParams()
172 | entry := libtorrent.WriteResumeData(params)
173 | defer libtorrent.DeleteEntry(entry)
174 |
175 | bEncoded := []byte(libtorrent.Bencode(entry))
176 | if _, e1 := DecodeTorrentData(bEncoded); e1 == nil {
177 | if e2 := ioutil.WriteFile(s.fastResumeFilePath(infoHash), bEncoded, 0644); e2 != nil {
178 | log.Errorf("Failed saving '%s.fastresume': %s", infoHash, e2)
179 | }
180 | } else {
181 | log.Warningf("Resume data corrupted for %s, %d bytes received and failed to decode with: %s",
182 | torrentStatus.GetName(), len(bEncoded), e1)
183 | }
184 | }
185 |
186 | func (s *Service) onMetadataReceived(alert libtorrent.MetadataReceivedAlert) {
187 | torrentInfo := alert.GetHandle().TorrentFile()
188 | infoHash := getInfoHash(torrentInfo.InfoHash())
189 |
190 | if torrent, err := s.GetTorrent(infoHash); err == nil {
191 | torrent.onMetadataReceived()
192 | } else {
193 | log.Errorf("Unable to get torrent with infohash %s. Skipping onMetadataReceived", infoHash)
194 | }
195 |
196 | log.Debugf("Saving %s.torrent", infoHash)
197 | torrentFile := libtorrent.NewCreateTorrent(torrentInfo)
198 | defer libtorrent.DeleteCreateTorrent(torrentFile)
199 | torrentContent := torrentFile.Generate()
200 | defer libtorrent.DeleteEntry(torrentContent)
201 |
202 | bEncodedTorrent := []byte(libtorrent.Bencode(torrentContent))
203 | if err := ioutil.WriteFile(s.torrentPath(infoHash), bEncodedTorrent, 0644); err == nil {
204 | s.deleteMagnetFile(infoHash)
205 | } else {
206 | log.Errorf("Failed saving '%s.torrent': %s", infoHash, err)
207 | }
208 | }
209 |
210 | func (s *Service) onStateChanged(alert libtorrent.StateChangedAlert) {
211 | switch alert.GetState() {
212 | case libtorrent.TorrentStatusDownloading:
213 | infoHash := getHandleInfoHash(alert.GetHandle())
214 | if torrent, err := s.GetTorrent(infoHash); err == nil {
215 | torrent.checkAvailableSpace()
216 | }
217 | }
218 | }
219 |
220 | func getHandleInfoHash(handle libtorrent.TorrentHandle) string {
221 | sha1Hash := handle.InfoHash()
222 | defer libtorrent.DeleteSha1_hash(sha1Hash)
223 | return getInfoHash(sha1Hash)
224 | }
225 |
226 | func getInfoHash(hash libtorrent.Sha1_hash) string {
227 | return hex.EncodeToString([]byte(hash.ToString()))
228 | }
229 |
230 | func (s *Service) saveResumeDataLoop() {
231 | defer s.wg.Done()
232 | for {
233 | select {
234 | case <-s.closing:
235 | return
236 | case <-time.After(s.config.SessionSave * time.Second):
237 | s.mu.RLock()
238 | for _, torrent := range s.torrents {
239 | if torrent.handle.IsValid() {
240 | status := torrent.handle.Status()
241 | if status.GetHasMetadata() && status.GetNeedSaveResume() {
242 | torrent.handle.SaveResumeData(libtorrent.TorrentHandleSaveInfoDict)
243 | }
244 | libtorrent.DeleteTorrentStatus(status)
245 | }
246 | }
247 | s.mu.RUnlock()
248 | }
249 | }
250 | }
251 |
252 | func (s *Service) stopServices() {
253 | log.Debug("Stopping LSD/DHT/UPNP/NAT-PPM")
254 | s.settingsPack.SetBool("enable_lsd", false)
255 | s.settingsPack.SetBool("enable_dht", false)
256 | s.settingsPack.SetBool("enable_upnp", false)
257 | s.settingsPack.SetBool("enable_natpmp", false)
258 | s.session.ApplySettings(s.settingsPack)
259 | }
260 |
261 | func (s *Service) removeTorrents() {
262 | for _, torrent := range s.torrents {
263 | torrent.remove(false)
264 | }
265 | s.torrents = nil
266 | }
267 |
268 | func (s *Service) Close() {
269 | log.Info("Stopping Service")
270 | s.stopServices()
271 |
272 | log.Debug("Closing service routines")
273 | close(s.closing)
274 | s.wg.Wait()
275 |
276 | log.Debug("Destroying service")
277 | s.removeTorrents()
278 | libtorrent.DeleteSession(s.session)
279 | libtorrent.DeleteSettingsPack(s.settingsPack)
280 | }
281 |
282 | func (s *Service) Reconfigure(config *settings.Settings, reset bool) {
283 | log.Info("Reconfiguring Service")
284 | s.mu.Lock()
285 | defer s.mu.Unlock()
286 |
287 | createDir(config.DownloadPath)
288 | createDir(config.TorrentsPath)
289 |
290 | s.configure(config)
291 |
292 | if reset {
293 | log.Debug("Resetting torrents")
294 | s.removeTorrents()
295 | s.loadTorrentFiles()
296 | }
297 | }
298 |
299 | func (s *Service) configure(config *settings.Settings) {
300 | s.config = config
301 |
302 | logging.SetLevel(s.config.ServiceLogLevel, log.Module)
303 | logging.SetLevel(s.config.AlertsLogLevel, alertsLog.Module)
304 |
305 | log.Info("Applying session settings")
306 |
307 | s.UserAgent = util.UserAgent()
308 | if s.config.UserAgent > 0 {
309 | switch s.config.UserAgent {
310 | case settings.LibtorrentUA:
311 | s.UserAgent = "libtorrent/" + libtorrent.Version()
312 | case settings.LibtorrentRasterbar_1_1_0_UA:
313 | s.UserAgent = "libtorrent (Rasterbar) 1.1.0"
314 | case settings.BitTorrent_7_5_0_UA:
315 | s.UserAgent = "BitTorrent 7.5.0"
316 | case settings.BitTorrent_7_4_3_UA:
317 | s.UserAgent = "BitTorrent 7.4.3"
318 | case settings.UTorrent_3_4_9_UA:
319 | s.UserAgent = "µTorrent 3.4.9"
320 | case settings.UTorrent_3_2_0_UA:
321 | s.UserAgent = "µTorrent 3.2.0"
322 | case settings.UTorrent_2_2_1_UA:
323 | s.UserAgent = "µTorrent 2.2.1"
324 | case settings.Transmission_2_92_UA:
325 | s.UserAgent = "Transmission 2.92"
326 | case settings.Deluge_1_3_6_0_UA:
327 | s.UserAgent = "Deluge 1.3.6.0"
328 | case settings.Deluge_1_3_12_0_UA:
329 | s.UserAgent = "Deluge 1.3.12.0"
330 | case settings.Vuze_5_7_3_0_UA:
331 | s.UserAgent = "Vuze 5.7.3.0"
332 | default:
333 | log.Warning("Invalid user agent provided: using default")
334 | }
335 | }
336 | log.Infof("UserAgent: %s", s.UserAgent)
337 |
338 | if s.config.UserAgent != settings.LibtorrentUA {
339 | s.settingsPack.SetStr("user_agent", s.UserAgent)
340 | }
341 | s.settingsPack.SetInt("request_timeout", 2)
342 | s.settingsPack.SetInt("peer_connect_timeout", 2)
343 | s.settingsPack.SetBool("strict_end_game_mode", true)
344 | s.settingsPack.SetBool("announce_to_all_trackers", true)
345 | s.settingsPack.SetBool("announce_to_all_tiers", true)
346 | s.settingsPack.SetInt("connection_speed", 500)
347 | s.settingsPack.SetInt("choking_algorithm", 0)
348 | s.settingsPack.SetInt("share_ratio_limit", 0)
349 | s.settingsPack.SetInt("seed_time_ratio_limit", 0)
350 | s.settingsPack.SetInt("seed_time_limit", 0)
351 | s.settingsPack.SetInt("peer_tos", ipToSLowCost)
352 | s.settingsPack.SetInt("torrent_connect_boost", 0)
353 | s.settingsPack.SetBool("rate_limit_ip_overhead", true)
354 | s.settingsPack.SetBool("no_atime_storage", true)
355 | s.settingsPack.SetBool("announce_double_nat", true)
356 | s.settingsPack.SetBool("prioritize_partial_pieces", false)
357 | s.settingsPack.SetBool("free_torrent_hashes", true)
358 | s.settingsPack.SetBool("use_parole_mode", true)
359 | s.settingsPack.SetInt("seed_choking_algorithm", int(libtorrent.SettingsPackFastestUpload))
360 | s.settingsPack.SetBool("upnp_ignore_nonrouters", true)
361 | s.settingsPack.SetBool("lazy_bitfields", true)
362 | s.settingsPack.SetInt("stop_tracker_timeout", 1)
363 | s.settingsPack.SetInt("auto_scrape_interval", 1200)
364 | s.settingsPack.SetInt("auto_scrape_min_interval", 900)
365 | s.settingsPack.SetBool("ignore_limits_on_local_network", true)
366 | s.settingsPack.SetBool("rate_limit_utp", true)
367 | s.settingsPack.SetInt("mixed_mode_algorithm", int(libtorrent.SettingsPackPreferTcp))
368 |
369 | // For Android external storage / OS-mounted NAS setups
370 | if s.config.TunedStorage {
371 | s.settingsPack.SetBool("use_read_cache", true)
372 | s.settingsPack.SetBool("coalesce_reads", true)
373 | s.settingsPack.SetBool("coalesce_writes", true)
374 | s.settingsPack.SetInt("max_queued_disk_bytes", 10*1024*1024)
375 | s.settingsPack.SetInt("cache_size", -1)
376 | }
377 |
378 | if s.config.ConnectionsLimit > 0 {
379 | s.settingsPack.SetInt("connections_limit", s.config.ConnectionsLimit)
380 | } else {
381 | setPlatformSpecificSettings(s.settingsPack)
382 | }
383 |
384 | if !s.config.LimitAfterBuffering || s.rateLimited {
385 | s.settingsPack.SetInt("download_rate_limit", s.config.MaxDownloadRate)
386 | s.settingsPack.SetInt("upload_rate_limit", s.config.MaxUploadRate)
387 | s.rateLimited = true
388 | }
389 |
390 | if s.config.ShareRatioLimit > 0 {
391 | s.settingsPack.SetInt("share_ratio_limit", s.config.ShareRatioLimit)
392 | }
393 | if s.config.SeedTimeRatioLimit > 0 {
394 | s.settingsPack.SetInt("seed_time_ratio_limit", s.config.SeedTimeRatioLimit)
395 | }
396 | if s.config.SeedTimeLimit > 0 {
397 | s.settingsPack.SetInt("seed_time_limit", s.config.SeedTimeLimit)
398 | }
399 |
400 | s.settingsPack.SetInt("active_downloads", s.config.ActiveDownloadsLimit)
401 | s.settingsPack.SetInt("active_seeds", s.config.ActiveSeedsLimit)
402 | s.settingsPack.SetInt("active_checking", s.config.ActiveCheckingLimit)
403 | s.settingsPack.SetInt("active_dht_limit", s.config.ActiveDhtLimit)
404 | s.settingsPack.SetInt("active_tracker_limit", s.config.ActiveTrackerLimit)
405 | s.settingsPack.SetInt("active_lsd_limit", s.config.ActiveLsdLimit)
406 | s.settingsPack.SetInt("active_limit", s.config.ActiveLimit)
407 |
408 | if s.config.EncryptionPolicy == settings.EncryptionDisabledPolicy ||
409 | s.config.EncryptionPolicy == settings.EncryptionForcedPolicy {
410 | log.Debug("Applying encryption settings")
411 | var policy int
412 | var level int
413 | var preferRc4 bool
414 |
415 | switch s.config.EncryptionPolicy {
416 | case settings.EncryptionDisabledPolicy:
417 | policy = int(libtorrent.SettingsPackPeDisabled)
418 | level = int(libtorrent.SettingsPackPeBoth)
419 | preferRc4 = false
420 | case settings.EncryptionForcedPolicy:
421 | policy = int(libtorrent.SettingsPackPeForced)
422 | level = int(libtorrent.SettingsPackPeRc4)
423 | preferRc4 = true
424 | }
425 |
426 | s.settingsPack.SetInt("out_enc_policy", policy)
427 | s.settingsPack.SetInt("in_enc_policy", policy)
428 | s.settingsPack.SetInt("allowed_enc_level", level)
429 | s.settingsPack.SetBool("prefer_rc4", preferRc4)
430 | } else if s.config.EncryptionPolicy != settings.EncryptionEnabledPolicy {
431 | log.Warning("Invalid encryption policy provided. Using default")
432 | }
433 |
434 | if s.config.Proxy != nil && s.config.Proxy.Type != settings.ProxyTypeNone {
435 | log.Debug("Applying proxy settings")
436 | s.settingsPack.SetInt("proxy_type", int(s.config.Proxy.Type))
437 | s.settingsPack.SetInt("proxy_port", s.config.Proxy.Port)
438 | s.settingsPack.SetStr("proxy_hostname", s.config.Proxy.Hostname)
439 | s.settingsPack.SetStr("proxy_username", s.config.Proxy.Username)
440 | s.settingsPack.SetStr("proxy_password", s.config.Proxy.Password)
441 | s.settingsPack.SetBool("proxy_tracker_connections", true)
442 | s.settingsPack.SetBool("proxy_peer_connections", true)
443 | s.settingsPack.SetBool("proxy_hostnames", true)
444 | s.settingsPack.SetBool("force_proxy", true)
445 | if s.config.Proxy.Type == settings.ProxyTypeI2PSAM {
446 | s.settingsPack.SetInt("i2p_port", s.config.Proxy.Port)
447 | s.settingsPack.SetStr("i2p_hostname", s.config.Proxy.Hostname)
448 | s.settingsPack.SetBool("allows_i2p_mixed", false)
449 | s.settingsPack.SetBool("allows_i2p_mixed", true)
450 | }
451 | }
452 |
453 | // Set alert_mask here so it also applies on reconfigure...
454 | s.settingsPack.SetInt("alert_mask", int(
455 | libtorrent.AlertStatusNotification|
456 | libtorrent.AlertStorageNotification|
457 | libtorrent.AlertErrorNotification))
458 |
459 | // Start services
460 | var listenInterfaces []string
461 | if interfaces := strings.Replace(s.config.ListenInterfaces, " ", "", -1); interfaces != "" {
462 | listenInterfaces = strings.Split(interfaces, ",")
463 | } else {
464 | listenInterfaces = []string{"0.0.0.0", "[::]"}
465 | }
466 |
467 | listenPort := strconv.FormatUint(uint64(s.config.ListenPort), 10)
468 | for i, listenInterface := range listenInterfaces {
469 | if !portRegex.MatchString(listenInterface) {
470 | listenInterfaces[i] += ":" + listenPort
471 | }
472 | }
473 | s.settingsPack.SetStr("listen_interfaces", strings.Join(listenInterfaces, ","))
474 |
475 | if outInterfaces := strings.Replace(s.config.OutgoingInterfaces, " ", "", -1); outInterfaces != "" {
476 | s.settingsPack.SetStr("outgoing_interfaces", outInterfaces)
477 | }
478 |
479 | s.settingsPack.SetStr("dht_bootstrap_nodes", strings.Join(DefaultDhtBootstrapNodes, ","))
480 | s.settingsPack.SetBool("enable_dht", !s.config.DisableDHT)
481 | s.settingsPack.SetBool("enable_upnp", !s.config.DisableUPNP)
482 | s.settingsPack.SetBool("enable_natpmp", !s.config.DisableNatPMP)
483 | s.settingsPack.SetBool("enable_lsd", !s.config.DisableLSD)
484 |
485 | if s.session == nil {
486 | log.Debug("First configuration, starting a new session")
487 | s.session = libtorrent.NewSession(s.settingsPack, libtorrent.SessionHandleAddDefaultPlugins)
488 | } else {
489 | log.Debug("Modifying session settings")
490 | s.session.ApplySettings(s.settingsPack)
491 | }
492 | }
493 |
494 | func (s *Service) setBufferingRateLimit(enable bool) {
495 | if s.config.LimitAfterBuffering && enable != s.rateLimited {
496 | log.Debugf("Setting rate limits, enable=%t", enable)
497 | if enable {
498 | s.settingsPack.SetInt("download_rate_limit", s.config.MaxDownloadRate)
499 | s.settingsPack.SetInt("upload_rate_limit", s.config.MaxUploadRate)
500 | } else {
501 | s.settingsPack.SetInt("download_rate_limit", 0)
502 | s.settingsPack.SetInt("upload_rate_limit", 0)
503 | }
504 | s.session.ApplySettings(s.settingsPack)
505 | s.rateLimited = enable
506 | }
507 | }
508 |
509 | func (s *Service) addTorrentWithParams(torrentParams libtorrent.AddTorrentParams, infoHash string, isResumeData, noDownload bool) error {
510 | log.Debugf("Adding torrent params with infohash %s", infoHash)
511 |
512 | if !isResumeData {
513 | log.Debugf("Setting params for '%s' torrent", infoHash)
514 | torrentParams.SetSavePath(s.config.DownloadPath)
515 | // torrentParams.SetStorageMode(libtorrent.StorageModeAllocate)
516 | torrentParams.SetFlags(torrentParams.GetFlags() | libtorrent.GetSequentialDownload())
517 | }
518 |
519 | if noDownload {
520 | log.Debugf("Disabling download for '%s' torrent", infoHash)
521 | filesPriorities := libtorrent.NewStdVectorChar()
522 | defer libtorrent.DeleteStdVectorChar(filesPriorities)
523 | for i := maxFilesPerTorrent; i > 0; i-- {
524 | filesPriorities.Add(0)
525 | }
526 | torrentParams.SetFilePriorities(filesPriorities)
527 | }
528 |
529 | if _, _, e := s.getTorrent(infoHash); e == nil {
530 | return DuplicateTorrentError
531 | } else {
532 | errorCode := libtorrent.NewErrorCode()
533 | defer libtorrent.DeleteErrorCode(errorCode)
534 | torrentHandle := s.session.AddTorrent(torrentParams, errorCode)
535 | if errorCode.Failed() || !torrentHandle.IsValid() {
536 | if torrentHandle.Swigcptr() != 0 {
537 | libtorrent.DeleteTorrentHandle(torrentHandle)
538 | }
539 | log.Errorf("Error adding torrent '%s': %v", infoHash, errorCode.Message())
540 | return LoadTorrentError
541 | } else {
542 | s.torrents = append(s.torrents, NewTorrent(s, torrentHandle, infoHash))
543 | }
544 | }
545 | return nil
546 | }
547 |
548 | func (s *Service) AddMagnet(magnet string, download bool) (infoHash string, err error) {
549 | s.mu.Lock()
550 | defer s.mu.Unlock()
551 | return s.addMagnet(magnet, download, true)
552 | }
553 |
554 | func (s *Service) addMagnet(magnet string, download, saveMagnet bool) (infoHash string, err error) {
555 | log.Debugf("Adding magnet '%s' with download=%t and save=%t", magnet, download, saveMagnet)
556 | torrentParams := libtorrent.NewAddTorrentParams()
557 | defer libtorrent.DeleteAddTorrentParams(torrentParams)
558 | errorCode := libtorrent.NewErrorCode()
559 | defer libtorrent.DeleteErrorCode(errorCode)
560 |
561 | libtorrent.ParseMagnetUri(magnet, torrentParams, errorCode)
562 | if errorCode.Failed() {
563 | return "", errors.New(errorCode.Message().(string))
564 | }
565 |
566 | infoHash = getInfoHash(torrentParams.GetInfoHash())
567 | err = s.addTorrentWithParams(torrentParams, infoHash, false, !download)
568 | if err == nil && saveMagnet {
569 | if e := saveGobData(s.magnetFilePath(infoHash), Magnet{magnet, download}, 0644); e != nil {
570 | log.Errorf("Failed saving magnet: %s", e)
571 | }
572 | }
573 | return
574 | }
575 |
576 | func (s *Service) AddTorrentData(data []byte, download bool) (infoHash string, err error) {
577 | log.Debugf("Adding torrent data with download=%t", download)
578 | errorCode := libtorrent.NewErrorCode()
579 | defer libtorrent.DeleteErrorCode(errorCode)
580 | info := libtorrent.NewTorrentInfo(string(data), len(data), errorCode)
581 | defer libtorrent.DeleteTorrentInfo(info)
582 |
583 | if errorCode.Failed() {
584 | return "", errors.New(errorCode.Message().(string))
585 | }
586 |
587 | torrentParams := libtorrent.NewAddTorrentParams()
588 | defer libtorrent.DeleteAddTorrentParams(torrentParams)
589 | torrentParams.SetTorrentInfo(info)
590 | infoHash = getInfoHash(info.InfoHash())
591 |
592 | s.mu.Lock()
593 | defer s.mu.Unlock()
594 | err = s.addTorrentWithParams(torrentParams, infoHash, false, !download)
595 | if err == nil {
596 | if e := ioutil.WriteFile(s.torrentPath(infoHash), data, 0644); e != nil {
597 | log.Errorf("Failed saving torrent: %s", e)
598 | }
599 | }
600 | return
601 | }
602 |
603 | func (s *Service) AddTorrentFile(torrentFile string, download bool) (infoHash string, err error) {
604 | s.mu.Lock()
605 | defer s.mu.Unlock()
606 | return s.addTorrentFile(torrentFile, download)
607 | }
608 |
609 | func (s *Service) addTorrentFile(torrentFile string, download bool) (infoHash string, err error) {
610 | log.Debugf("Adding torrent file '%s' with download=%t", torrentFile, download)
611 | fi, e := os.Stat(torrentFile)
612 | if e != nil {
613 | return "", e
614 | }
615 |
616 | errorCode := libtorrent.NewErrorCode()
617 | defer libtorrent.DeleteErrorCode(errorCode)
618 | info := libtorrent.NewTorrentInfo(torrentFile, errorCode)
619 | defer libtorrent.DeleteTorrentInfo(info)
620 |
621 | if errorCode.Failed() {
622 | return "", errors.New(errorCode.Message().(string))
623 | }
624 |
625 | torrentParams := libtorrent.NewAddTorrentParams()
626 | defer libtorrent.DeleteAddTorrentParams(torrentParams)
627 | torrentParams.SetTorrentInfo(info)
628 | infoHash = getInfoHash(info.InfoHash())
629 |
630 | err = s.addTorrentWithParams(torrentParams, infoHash, false, !download)
631 | if err == nil {
632 | torrentDst := s.torrentPath(infoHash)
633 | if fi2, e1 := os.Stat(torrentDst); e1 != nil || !os.SameFile(fi, fi2) {
634 | log.Debugf("Copying torrent %s", infoHash)
635 | if e2 := copyFileContents(torrentFile, torrentDst); e2 != nil {
636 | log.Errorf("Failed copying torrent: %s", e2)
637 | }
638 | }
639 | }
640 | return
641 | }
642 |
643 | func (s *Service) addTorrentWithResumeData(fastResumeFile string) (err error) {
644 | log.Debugf("Adding torrent with resume data '%s'", fastResumeFile)
645 | if fastResumeData, e := ioutil.ReadFile(fastResumeFile); e != nil {
646 | deleteFile(fastResumeFile)
647 | err = e
648 | } else {
649 | node := libtorrent.NewBdecodeNode()
650 | defer libtorrent.DeleteBdecodeNode(node)
651 | errorCode := libtorrent.NewErrorCode()
652 | defer libtorrent.DeleteErrorCode(errorCode)
653 | libtorrent.Bdecode(fastResumeData, int64(len(fastResumeData)), node, errorCode)
654 | if errorCode.Failed() {
655 | err = errors.New(errorCode.Message().(string))
656 | } else {
657 | torrentParams := libtorrent.ReadResumeData(node, errorCode)
658 | defer libtorrent.DeleteAddTorrentParams(torrentParams)
659 | if errorCode.Failed() {
660 | err = errors.New(errorCode.Message().(string))
661 | } else {
662 | infoHash := getInfoHash(torrentParams.GetInfoHash())
663 | err = s.addTorrentWithParams(torrentParams, infoHash, true, false)
664 | }
665 | }
666 | }
667 | return
668 | }
669 |
670 | func (s *Service) loadTorrentFiles() {
671 | resumeFiles, _ := filepath.Glob(s.fastResumeFilePath("*"))
672 | for _, fastResumeFile := range resumeFiles {
673 | if err := s.addTorrentWithResumeData(fastResumeFile); err != nil {
674 | log.Errorf("Failed adding torrent with resume data: %s", err)
675 | }
676 | }
677 |
678 | files, _ := filepath.Glob(s.torrentPath("*"))
679 | for _, torrentFile := range files {
680 | if infoHash, err := s.addTorrentFile(torrentFile, false); err == LoadTorrentError {
681 | s.deletePartsFile(infoHash)
682 | s.deleteFastResumeFile(infoHash)
683 | s.deleteTorrentFile(infoHash)
684 | }
685 | }
686 |
687 | magnets, _ := filepath.Glob(s.magnetFilePath("*"))
688 | for _, magnet := range magnets {
689 | data := Magnet{}
690 | if err := readGobData(magnet, &data); err == nil {
691 | if infoHash, e1 := s.addMagnet(data.Uri, data.Download, false); e1 == DuplicateTorrentError {
692 | if _, t, e2 := s.getTorrent(infoHash); e2 == nil && t.hasMetadata {
693 | deleteFile(magnet)
694 | }
695 | }
696 | } else {
697 | log.Errorf("Failed to read magnet file '%s': %s", magnet, err)
698 | }
699 | }
700 |
701 | partsFiles, _ := filepath.Glob(s.partsFilePath("*"))
702 | for _, partsFile := range partsFiles {
703 | infoHash := strings.TrimPrefix(strings.TrimSuffix(filepath.Base(partsFile), extParts), ".")
704 | if _, _, err := s.getTorrent(infoHash); err != nil {
705 | log.Debugf("Cleaning up stale parts file '%s'", partsFiles)
706 | deleteFile(partsFile)
707 | }
708 | }
709 | }
710 |
711 | func (s *Service) downloadProgress() {
712 | defer s.wg.Done()
713 | progressTicker := time.NewTicker(libtorrentProgressTime)
714 | defer progressTicker.Stop()
715 |
716 | for {
717 | select {
718 | case <-s.closing:
719 | return
720 | case <-progressTicker.C:
721 | if s.session.IsPaused() {
722 | continue
723 | }
724 |
725 | var totalDownloadRate int64
726 | var totalUploadRate int64
727 | var totalProgressSize float64
728 | var totalSize int64
729 |
730 | hasFilesBuffering := false
731 |
732 | s.mu.Lock()
733 |
734 | for _, t := range s.torrents {
735 | if t.isPaused || !t.hasMetadata || !t.handle.IsValid() {
736 | continue
737 | }
738 |
739 | for _, f := range t.files {
740 | if f.verifyBufferingState() {
741 | hasFilesBuffering = true
742 | }
743 | }
744 |
745 | torrentStatus := t.handle.Status(libtorrent.TorrentHandleQueryName)
746 | totalDownloadRate += int64(torrentStatus.GetDownloadRate())
747 | totalUploadRate += int64(torrentStatus.GetUploadRate())
748 |
749 | progress := float64(torrentStatus.GetProgress())
750 | if progress < 1 {
751 | size := torrentStatus.GetTotalWanted()
752 | totalProgressSize += progress * float64(size)
753 | totalSize += size
754 | } else {
755 | seedingTime := torrentStatus.GetSeedingDuration()
756 | if progress == 1 && seedingTime == 0 {
757 | seedingTime = torrentStatus.GetFinishedDuration()
758 | }
759 | downloadTime := torrentStatus.GetActiveDuration() - seedingTime
760 | allTimeDownload := torrentStatus.GetAllTimeDownload()
761 |
762 | if s.config.SeedTimeLimit > 0 && seedingTime >= int64(s.config.SeedTimeLimit) {
763 | log.Infof("Seeding time limit reached, pausing %s", torrentStatus.GetName())
764 | t.Pause()
765 | } else if s.config.SeedTimeRatioLimit > 0 && downloadTime > 0 &&
766 | seedingTime*100/downloadTime >= int64(s.config.SeedTimeRatioLimit) {
767 | log.Infof("Seeding time ratio reached, pausing %s", torrentStatus.GetName())
768 | t.Pause()
769 | } else if s.config.ShareRatioLimit > 0 && allTimeDownload > 0 &&
770 | torrentStatus.GetAllTimeUpload()*100/allTimeDownload >= int64(s.config.ShareRatioLimit) {
771 | log.Infof("Share ratio reached, pausing %s", torrentStatus.GetName())
772 | t.Pause()
773 | }
774 | }
775 |
776 | libtorrent.DeleteTorrentStatus(torrentStatus)
777 | }
778 |
779 | s.setBufferingRateLimit(!hasFilesBuffering)
780 |
781 | s.downloadRate = totalDownloadRate
782 | s.uploadRate = totalUploadRate
783 | if totalSize > 0 {
784 | s.progress = 100 * totalProgressSize / float64(totalSize)
785 | } else {
786 | s.progress = 100
787 | }
788 |
789 | s.mu.Unlock()
790 | }
791 | }
792 | }
793 |
794 | func (s *Service) Pause() {
795 | s.session.Pause()
796 | }
797 |
798 | func (s *Service) Resume() {
799 | s.session.Resume()
800 | }
801 |
802 | func (s *Service) GetStatus() *ServiceStatus {
803 | s.mu.RLock()
804 | defer s.mu.RUnlock()
805 | return &ServiceStatus{
806 | Progress: s.progress,
807 | DownloadRate: s.downloadRate,
808 | UploadRate: s.uploadRate,
809 | NumTorrents: len(s.torrents),
810 | IsPaused: s.session.IsPaused(),
811 | }
812 | }
813 |
814 | func (s *Service) Torrents() []*Torrent {
815 | s.mu.RLock()
816 | defer s.mu.RUnlock()
817 | torrents := make([]*Torrent, len(s.torrents))
818 | copy(torrents, s.torrents)
819 | return torrents
820 | }
821 |
822 | func (s *Service) getTorrent(infoHash string) (int, *Torrent, error) {
823 | for i, t := range s.torrents {
824 | if t.infoHash == infoHash {
825 | return i, t, nil
826 | }
827 | }
828 | return -1, nil, InvalidInfoHashError
829 | }
830 |
831 | func (s *Service) GetTorrent(infoHash string) (*Torrent, error) {
832 | s.mu.RLock()
833 | defer s.mu.RUnlock()
834 | _, t, e := s.getTorrent(infoHash)
835 | return t, e
836 | }
837 |
838 | func (s *Service) RemoveTorrent(infoHash string, removeFiles bool) error {
839 | log.Debugf("Removing torrent with infohash %s and removeFiles=%t", infoHash, removeFiles)
840 | s.mu.Lock()
841 | defer s.mu.Unlock()
842 |
843 | index, torrent, err := s.getTorrent(infoHash)
844 | if err == nil {
845 | s.deletePartsFile(infoHash)
846 | s.deleteFastResumeFile(infoHash)
847 | s.deleteTorrentFile(infoHash)
848 | s.deleteMagnetFile(infoHash)
849 | s.torrents = append(s.torrents[:index], s.torrents[index+1:]...)
850 | torrent.remove(removeFiles)
851 | }
852 |
853 | return err
854 | }
855 |
856 | func (s *Service) partsFilePath(infoHash string) string {
857 | return filepath.Join(s.config.DownloadPath, "."+infoHash+extParts)
858 | }
859 |
860 | func (s *Service) deletePartsFile(infoHash string) {
861 | deleteFile(s.partsFilePath(infoHash))
862 | }
863 |
864 | func (s *Service) fastResumeFilePath(infoHash string) string {
865 | return filepath.Join(s.config.TorrentsPath, infoHash+extFastResume)
866 | }
867 |
868 | func (s *Service) deleteFastResumeFile(infoHash string) {
869 | deleteFile(s.fastResumeFilePath(infoHash))
870 | }
871 |
872 | func (s *Service) torrentPath(infoHash string) string {
873 | return filepath.Join(s.config.TorrentsPath, infoHash+extTorrent)
874 | }
875 |
876 | func (s *Service) deleteTorrentFile(infoHash string) {
877 | deleteFile(s.torrentPath(infoHash))
878 | }
879 |
880 | func (s *Service) magnetFilePath(infoHash string) string {
881 | return filepath.Join(s.config.TorrentsPath, infoHash+extMagnet)
882 | }
883 |
884 | func (s *Service) deleteMagnetFile(infoHash string) {
885 | deleteFile(s.magnetFilePath(infoHash))
886 | }
887 |
--------------------------------------------------------------------------------
/bittorrent/service_all.go:
--------------------------------------------------------------------------------
1 | // +build !arm
2 |
3 | package bittorrent
4 |
5 | import "github.com/i96751414/libtorrent-go"
6 |
7 | // Nothing to do on regular devices
8 | //noinspection GoUnusedParameter
9 | func setPlatformSpecificSettings(_ libtorrent.SettingsPack) {
10 | }
11 |
--------------------------------------------------------------------------------
/bittorrent/service_arm.go:
--------------------------------------------------------------------------------
1 | // +build arm
2 |
3 | package bittorrent
4 |
5 | import (
6 | "runtime"
7 |
8 | "github.com/i96751414/libtorrent-go"
9 | )
10 |
11 | const (
12 | maxSingleCoreConnections = 50
13 | )
14 |
15 | // On Raspberry Pi, we need to limit the number of active connections
16 | // because otherwise it fries. So here we need to detect that we are on RPi
17 | // (or, rather, a single cpu arm machine, no need to be specific to RPi) and
18 | // set those limits.
19 | func setPlatformSpecificSettings(settingsPack libtorrent.SettingsPack) {
20 | if runtime.NumCPU() == 1 { // single core?
21 | log.Debug("Setting max single core connections limit")
22 | settingsPack.SetInt("connections_limit", maxSingleCoreConnections)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/bittorrent/torrent.go:
--------------------------------------------------------------------------------
1 | package bittorrent
2 |
3 | import (
4 | "bytes"
5 | "runtime"
6 | "sync"
7 |
8 | "github.com/dustin/go-humanize"
9 | "github.com/i96751414/libtorrent-go"
10 | "github.com/i96751414/torrest/diskusage"
11 | "github.com/zeebo/bencode"
12 | )
13 |
14 | type LTStatus int
15 |
16 | //noinspection GoUnusedConst
17 | const (
18 | QueuedStatus LTStatus = iota // libtorrent.TorrentStatusUnusedEnumForBackwardsCompatibility
19 | CheckingStatus // libtorrent.TorrentStatusCheckingFiles
20 | FindingStatus // libtorrent.TorrentStatusDownloadingMetadata
21 | DownloadingStatus // libtorrent.TorrentStatusDownloading
22 | FinishedStatus // libtorrent.TorrentStatusFinished
23 | SeedingStatus // libtorrent.TorrentStatusSeeding
24 | AllocatingStatus // libtorrent.TorrentStatusAllocating
25 | CheckingResumeDataStatus // libtorrent.TorrentStatusCheckingResumeData
26 | // Custom status
27 | PausedStatus
28 | BufferingStatus
29 | )
30 |
31 | //noinspection GoUnusedConst
32 | const (
33 | DontDownloadPriority = uint(0)
34 | LowPriority = uint(1)
35 | DefaultPriority = uint(4)
36 | HighPriority = uint(6)
37 | TopPriority = uint(7)
38 | )
39 |
40 | type Torrent struct {
41 | service *Service
42 | handle libtorrent.TorrentHandle
43 | infoHash string
44 | defaultName string
45 | mu *sync.RWMutex
46 | closing chan interface{}
47 | isPaused bool
48 | files []*File
49 | spaceChecked bool
50 | hasMetadata bool
51 | }
52 |
53 | type TorrentInfo struct {
54 | InfoHash string `json:"info_hash"`
55 | Name string `json:"name"`
56 | Size int64 `json:"size"`
57 | }
58 |
59 | type TorrentStatus struct {
60 | Total int64 `json:"total"`
61 | TotalDone int64 `json:"total_done"`
62 | TotalWanted int64 `json:"total_wanted"`
63 | TotalWantedDone int64 `json:"total_wanted_done"`
64 | Progress float64 `json:"progress"`
65 | DownloadRate int `json:"download_rate"`
66 | UploadRate int `json:"upload_rate"`
67 | Paused bool `json:"paused"`
68 | HasMetadata bool `json:"has_metadata"`
69 | State LTStatus `json:"state"`
70 | Seeders int `json:"seeders"`
71 | SeedersTotal int `json:"seeders_total"`
72 | Peers int `json:"peers"`
73 | PeersTotal int `json:"peers_total"`
74 | SeedingTime int64 `json:"seeding_time"`
75 | FinishedTime int64 `json:"finished_time"`
76 | ActiveTime int64 `json:"active_time"`
77 | AllTimeDownload int64 `json:"all_time_download"`
78 | AllTimeUpload int64 `json:"all_time_upload"`
79 | }
80 |
81 | type TorrentFileRaw struct {
82 | Announce string `bencode:"announce"`
83 | AnnounceList [][]string `bencode:"announce-list"`
84 | Info map[string]interface{} `bencode:"info"`
85 | }
86 |
87 | func DecodeTorrentData(data []byte) (*TorrentFileRaw, error) {
88 | var torrentFile *TorrentFileRaw
89 | dec := bencode.NewDecoder(bytes.NewReader(data))
90 | if err := dec.Decode(&torrentFile); err != nil {
91 | return nil, err
92 | }
93 | return torrentFile, nil
94 | }
95 |
96 | func NewTorrent(service *Service, handle libtorrent.TorrentHandle, infoHash string) *Torrent {
97 | flags := handle.Flags()
98 | paused := hasFlagsUint64(flags, libtorrent.GetPaused()) && !hasFlagsUint64(flags, libtorrent.GetAutoManaged())
99 | status := handle.Status(libtorrent.TorrentHandleQueryName)
100 | defer libtorrent.DeleteTorrentStatus(status)
101 | name := status.GetName()
102 | if len(name) == 0 {
103 | name = infoHash
104 | }
105 |
106 | t := &Torrent{
107 | service: service,
108 | handle: handle,
109 | infoHash: infoHash,
110 | defaultName: name,
111 | mu: &sync.RWMutex{},
112 | closing: make(chan interface{}),
113 | isPaused: paused,
114 | }
115 |
116 | if status.GetHasMetadata() {
117 | t.onMetadataReceived()
118 | }
119 |
120 | runtime.SetFinalizer(t, func(torrent *Torrent) {
121 | libtorrent.DeleteTorrentHandle(torrent.handle)
122 | })
123 |
124 | return t
125 | }
126 |
127 | func (t *Torrent) onMetadataReceived() {
128 | info := t.handle.TorrentFile()
129 | files := info.Files()
130 | fileCount := info.NumFiles()
131 |
132 | f := make([]*File, fileCount)
133 | for i := 0; i < fileCount; i++ {
134 | f[i] = NewFile(t, files, i)
135 | }
136 |
137 | t.mu.Lock()
138 | defer t.mu.Unlock()
139 | t.files = f
140 | t.hasMetadata = true
141 | }
142 |
143 | func (t *Torrent) InfoHash() string {
144 | return t.infoHash
145 | }
146 |
147 | func (t *Torrent) Pause() {
148 | t.mu.Lock()
149 | defer t.mu.Unlock()
150 | t.handle.UnsetFlags(libtorrent.GetAutoManaged())
151 | t.handle.Pause(libtorrent.TorrentHandleClearDiskCache)
152 | t.isPaused = true
153 | }
154 |
155 | func (t *Torrent) Resume() {
156 | t.mu.Lock()
157 | defer t.mu.Unlock()
158 | t.handle.SetFlags(libtorrent.GetAutoManaged())
159 | t.isPaused = false
160 | }
161 |
162 | func (t *Torrent) getState(file ...*File) LTStatus {
163 | if t.isPaused {
164 | return PausedStatus
165 | }
166 | if hasFlagsUint64(t.handle.Flags(), libtorrent.GetPaused()|libtorrent.GetAutoManaged()) {
167 | return QueuedStatus
168 | }
169 | if !t.hasMetadata {
170 | return FindingStatus
171 | }
172 |
173 | status := t.handle.Status()
174 | defer libtorrent.DeleteTorrentStatus(status)
175 | state := LTStatus(status.GetState())
176 |
177 | if state == DownloadingStatus {
178 | downloading := false
179 | for _, f := range file {
180 | if f.isBuffering {
181 | return BufferingStatus
182 | }
183 | if f.priority != DontDownloadPriority {
184 | downloading = true
185 | }
186 | }
187 | if !downloading || t.getFilesProgress(file...) == 100 {
188 | return FinishedStatus
189 | }
190 | }
191 |
192 | return state
193 | }
194 |
195 | func (t *Torrent) GetState() LTStatus {
196 | return t.getState(t.files...)
197 | }
198 |
199 | func (t *Torrent) HasMetadata() bool {
200 | return t.hasMetadata
201 | }
202 |
203 | func (t *Torrent) GetInfo() *TorrentInfo {
204 | torrentInfo := &TorrentInfo{InfoHash: t.infoHash}
205 | if info := t.handle.TorrentFile(); info.Swigcptr() != 0 {
206 | torrentInfo.Name = info.Name()
207 | torrentInfo.Size = info.TotalSize()
208 | } else {
209 | torrentInfo.Name = t.defaultName
210 | }
211 | return torrentInfo
212 | }
213 |
214 | func (t *Torrent) GetStatus() *TorrentStatus {
215 | t.mu.RLock()
216 | defer t.mu.RUnlock()
217 | status := t.handle.Status()
218 | defer libtorrent.DeleteTorrentStatus(status)
219 |
220 | seeders := status.GetNumSeeds()
221 | seedersTotal := status.GetNumComplete()
222 | if seedersTotal < 0 {
223 | seedersTotal = seeders
224 | }
225 |
226 | peers := status.GetNumPeers() - seeders
227 | peersTotal := status.GetNumIncomplete()
228 | if peersTotal < 0 {
229 | peersTotal = peers
230 | }
231 |
232 | return &TorrentStatus{
233 | Total: status.GetTotal(),
234 | TotalDone: status.GetTotalDone(),
235 | TotalWanted: status.GetTotalWanted(),
236 | TotalWantedDone: status.GetTotalWantedDone(),
237 | Progress: float64(status.GetProgress()) * 100,
238 | DownloadRate: status.GetDownloadRate(),
239 | UploadRate: status.GetUploadRate(),
240 | Paused: t.isPaused,
241 | HasMetadata: t.hasMetadata,
242 | State: t.GetState(),
243 | Seeders: seeders,
244 | SeedersTotal: seedersTotal,
245 | Peers: peers,
246 | PeersTotal: peersTotal,
247 | SeedingTime: status.GetSeedingDuration(),
248 | FinishedTime: status.GetFinishedDuration(),
249 | ActiveTime: status.GetActiveDuration(),
250 | AllTimeDownload: status.GetAllTimeDownload(),
251 | AllTimeUpload: status.GetAllTimeUpload(),
252 | }
253 | }
254 |
255 | func (t *Torrent) Files() ([]*File, error) {
256 | if !t.hasMetadata {
257 | return nil, NoMetadataError
258 | }
259 | files := make([]*File, len(t.files))
260 | copy(files, t.files)
261 | return files, nil
262 | }
263 |
264 | func (t *Torrent) GetFile(id int) (*File, error) {
265 | if !t.hasMetadata {
266 | return nil, NoMetadataError
267 | }
268 | if id < 0 || id >= len(t.files) {
269 | return nil, InvalidFileIdError
270 | }
271 | return t.files[id], nil
272 | }
273 |
274 | func (t *Torrent) SetPriority(priority uint) error {
275 | log.Debugf("Setting torrent %s with priority %d", t.infoHash, priority)
276 | if !t.hasMetadata {
277 | return NoMetadataError
278 | }
279 | for _, f := range t.files {
280 | f.SetPriority(priority)
281 | }
282 | return nil
283 | }
284 |
285 | func (t *Torrent) getFilesDownloadedBytes() []int64 {
286 | pVec := libtorrent.NewStdVectorSizeType()
287 | defer libtorrent.DeleteStdVectorSizeType(pVec)
288 |
289 | t.handle.FileProgress(pVec, int(libtorrent.TorrentHandlePieceGranularity))
290 | progresses := make([]int64, pVec.Size())
291 | for i := 0; i < int(pVec.Size()); i++ {
292 | progresses[i] = pVec.Get(i)
293 | }
294 | return progresses
295 | }
296 |
297 | func (t *Torrent) piecesBytesMissing(pieces []int) (missing int64) {
298 | queue := libtorrent.NewStdVectorPartialPieceInfo()
299 | defer libtorrent.DeleteStdVectorPartialPieceInfo(queue)
300 | t.handle.GetDownloadQueue(queue)
301 | info := t.handle.TorrentFile()
302 |
303 | for _, piece := range pieces {
304 | if !t.handle.HavePiece(piece) {
305 | missing += int64(info.PieceSize(piece))
306 | }
307 | }
308 |
309 | for i := 0; i < int(queue.Size()); i++ {
310 | ppi := queue.Get(i)
311 | if containsInt(pieces, ppi.GetPieceIndex()) {
312 | blocks := ppi.Blocks()
313 | blocksInPiece := ppi.GetBlocksInPiece()
314 | for b := 0; b < blocksInPiece; b++ {
315 | missing -= int64(blocks.Getitem(b).GetBytesProgress())
316 | }
317 | }
318 | }
319 | return
320 | }
321 |
322 | func (t *Torrent) getFilesProgress(file ...*File) float64 {
323 | var total int64
324 | var completed int64
325 |
326 | progresses := t.getFilesDownloadedBytes()
327 | for _, f := range file {
328 | total += f.length
329 | completed += progresses[f.index]
330 | }
331 |
332 | if total == 0 {
333 | return 100
334 | }
335 |
336 | progress := float64(completed) / float64(total) * 100.0
337 | if progress > 100 {
338 | progress = 100
339 | }
340 |
341 | return progress
342 | }
343 |
344 | func (t *Torrent) checkAvailableSpace() {
345 | if t.spaceChecked || !t.service.config.CheckAvailableSpace {
346 | return
347 | }
348 | if diskStatus, err := diskusage.DiskUsage(t.service.config.DownloadPath); err != nil {
349 | log.Warningf("Unable to retrieve the free space for %s", t.service.config.DownloadPath)
350 | return
351 | } else if diskStatus != nil {
352 | status := t.handle.Status(libtorrent.TorrentHandleQueryAccurateDownloadCounters |
353 | libtorrent.TorrentHandleQuerySavePath | libtorrent.TorrentHandleQueryName)
354 | defer libtorrent.DeleteTorrentStatus(status)
355 |
356 | if !status.GetHasMetadata() {
357 | log.Warning("Missing torrent metadata to check available space")
358 | return
359 | }
360 |
361 | totalSize := status.GetTotal()
362 | totalDone := status.GetTotalDone()
363 | sizeLeft := totalSize - totalDone
364 | path := status.GetSavePath()
365 |
366 | log.Infof("Checking for sufficient space on %s", path)
367 | log.Infof("Total size: %s", humanize.Bytes(uint64(totalSize)))
368 | log.Infof("Total done size: %s", humanize.Bytes(uint64(totalDone)))
369 | log.Infof("Size left to download: %s", humanize.Bytes(uint64(sizeLeft)))
370 | log.Infof("Available space: %s", humanize.Bytes(uint64(diskStatus.Free)))
371 |
372 | if diskStatus.Free < sizeLeft {
373 | log.Errorf("Insufficient free space on %s: has %d, needs %d", path, diskStatus.Free, sizeLeft)
374 | log.Infof("Pausing torrent %s", status.GetName())
375 | t.Pause()
376 | } else {
377 | t.spaceChecked = true
378 | }
379 | }
380 | }
381 |
382 | func (t *Torrent) remove(removeFiles bool) {
383 | close(t.closing)
384 |
385 | var flags uint
386 | if removeFiles {
387 | flags |= libtorrent.SessionHandleDeleteFiles
388 | }
389 |
390 | t.service.session.RemoveTorrent(t.handle, flags)
391 | log.Debugf("Torrent %s removed and destroyed", t.infoHash)
392 | }
393 |
--------------------------------------------------------------------------------
/bittorrent/utils.go:
--------------------------------------------------------------------------------
1 | package bittorrent
2 |
3 | import (
4 | "encoding/gob"
5 | "io"
6 | "os"
7 | )
8 |
9 | func deleteFile(path string) {
10 | if _, err := os.Stat(path); err == nil {
11 | if err := os.Remove(path); err != nil {
12 | log.Errorf("Failed to remove file '%s': %s", path, err)
13 | }
14 | }
15 | }
16 |
17 | func createDir(path string) {
18 | if _, err := os.Stat(path); os.IsNotExist(err) {
19 | if err := os.Mkdir(path, 0755); err != nil {
20 | panic("Unable to create '" + path + "' folder: " + err.Error())
21 | }
22 | }
23 | }
24 |
25 | func copyFileContents(src, dst string) error {
26 | in, err := os.Open(src)
27 | if err != nil {
28 | return err
29 | }
30 | out, err := os.Create(dst)
31 | if err != nil {
32 | return err
33 | }
34 | if _, err = io.Copy(out, in); err == nil {
35 | err = out.Sync()
36 | }
37 | if e := out.Close(); err == nil {
38 | err = e
39 | }
40 | _ = in.Close()
41 | return err
42 | }
43 |
44 | func hasFlagsUint64(flags, f uint64) bool {
45 | return flags&f == f
46 | }
47 |
48 | func containsInt(arr []int, value int) bool {
49 | for _, a := range arr {
50 | if a == value {
51 | return true
52 | }
53 | }
54 | return false
55 | }
56 |
57 | func saveGobData(path string, data interface{}, perm os.FileMode) error {
58 | f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
59 | if err != nil {
60 | return err
61 | }
62 | err = gob.NewEncoder(f).Encode(data)
63 | if e := f.Close(); err == nil {
64 | err = e
65 | }
66 | return err
67 | }
68 |
69 | func readGobData(path string, data interface{}) error {
70 | f, err := os.Open(path)
71 | if err != nil {
72 | return err
73 | }
74 | err = gob.NewDecoder(f).Decode(data)
75 | if e := f.Close(); err == nil {
76 | err = e
77 | }
78 | return err
79 | }
80 |
81 | /*
82 | // Functions left for testing
83 | func saveJsonData(path string, data interface{}, perm os.FileMode) error {
84 | d, err := json.Marshal(data)
85 | if err == nil {
86 | err = ioutil.WriteFile(path, d, perm)
87 | }
88 | return err
89 | }
90 |
91 | func readJsonData(path string, data interface{}) error {
92 | d, err := ioutil.ReadFile(path)
93 | if err == nil {
94 | err = json.Unmarshal(d, data)
95 | }
96 | return err
97 | }
98 | */
99 |
--------------------------------------------------------------------------------
/diskusage/diskuage_posix.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package diskusage
4 |
5 | import (
6 | "syscall"
7 | )
8 |
9 | // disk usage of path/disk
10 | //noinspection GoRedundantConversion
11 | func DiskUsage(path string) (*DiskStatus, error) {
12 | fs := syscall.Statfs_t{}
13 | err := syscall.Statfs(path, &fs)
14 | if err != nil {
15 | return nil, err
16 | }
17 | status := &DiskStatus{
18 | All: int64(fs.Blocks) * int64(fs.Bsize),
19 | Free: int64(fs.Bfree) * int64(fs.Bsize),
20 | }
21 | status.Used = status.All - status.Free
22 | return status, nil
23 | }
24 |
--------------------------------------------------------------------------------
/diskusage/diskusage.go:
--------------------------------------------------------------------------------
1 | package diskusage
2 |
3 | type DiskStatus struct {
4 | All int64
5 | Used int64
6 | Free int64
7 | }
8 |
--------------------------------------------------------------------------------
/diskusage/diskusage_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package diskusage
4 |
5 | import (
6 | "errors"
7 | "syscall"
8 | "unsafe"
9 | )
10 |
11 | var (
12 | kernel32 syscall.Handle
13 | pGetDiskFreeSpaceEx uintptr
14 | )
15 |
16 | func init() {
17 | var err error
18 | kernel32, err = syscall.LoadLibrary("Kernel32.dll")
19 | if err != nil {
20 | panic("Unable to load Kernel32.dll")
21 | }
22 | pGetDiskFreeSpaceEx, err = syscall.GetProcAddress(kernel32, "GetDiskFreeSpaceExW")
23 | if err != nil {
24 | panic("Unable to get GetDiskFreeSpaceExW process")
25 | }
26 | }
27 |
28 | // disk usage of path/disk
29 | func DiskUsage(path string) (*DiskStatus, error) {
30 | lpDirectoryName, err := syscall.UTF16PtrFromString(path)
31 | if err != nil {
32 | return nil, err
33 | }
34 | lpFreeBytesAvailable := int64(0)
35 | lpTotalNumberOfBytes := int64(0)
36 | lpTotalNumberOfFreeBytes := int64(0)
37 | _, _, e := syscall.Syscall6(pGetDiskFreeSpaceEx, 4,
38 | uintptr(unsafe.Pointer(lpDirectoryName)),
39 | uintptr(unsafe.Pointer(&lpFreeBytesAvailable)),
40 | uintptr(unsafe.Pointer(&lpTotalNumberOfBytes)),
41 | uintptr(unsafe.Pointer(&lpTotalNumberOfFreeBytes)), 0, 0)
42 | if e != 0 {
43 | return nil, errors.New(e.Error())
44 | }
45 | status := &DiskStatus{
46 | All: lpTotalNumberOfBytes,
47 | Free: lpFreeBytesAvailable,
48 | }
49 | status.Used = status.All - status.Free
50 | return status, nil
51 | }
52 |
--------------------------------------------------------------------------------
/docs/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "description": "Torrent server with a REST API",
5 | "title": "Torrest API",
6 | "contact": {
7 | "name": "i96751414",
8 | "url": "https://github.com/i96751414/torrest",
9 | "email": "i96751414@gmail.com"
10 | },
11 | "license": {
12 | "name": "MIT",
13 | "url": "https://github.com/i96751414/torrest/blob/master/LICENSE"
14 | },
15 | "version": "1.0"
16 | },
17 | "basePath": "/",
18 | "paths": {
19 | "/add/magnet": {
20 | "get": {
21 | "description": "add magnet to service",
22 | "produces": [
23 | "application/json"
24 | ],
25 | "summary": "Add Magnet",
26 | "operationId": "add-magnet",
27 | "parameters": [
28 | {
29 | "type": "string",
30 | "description": "magnet URI",
31 | "name": "uri",
32 | "in": "query",
33 | "required": true
34 | },
35 | {
36 | "type": "boolean",
37 | "description": "ignore if duplicate",
38 | "name": "ignore_duplicate",
39 | "in": "query"
40 | },
41 | {
42 | "type": "boolean",
43 | "description": "start downloading",
44 | "name": "download",
45 | "in": "query"
46 | }
47 | ],
48 | "responses": {
49 | "200": {
50 | "description": "OK",
51 | "schema": {
52 | "$ref": "#/definitions/api.NewTorrentResponse"
53 | }
54 | },
55 | "400": {
56 | "description": "Bad Request",
57 | "schema": {
58 | "$ref": "#/definitions/api.ErrorResponse"
59 | }
60 | },
61 | "500": {
62 | "description": "Internal Server Error",
63 | "schema": {
64 | "$ref": "#/definitions/api.ErrorResponse"
65 | }
66 | }
67 | }
68 | }
69 | },
70 | "/add/torrent": {
71 | "post": {
72 | "description": "add torrent file to service",
73 | "consumes": [
74 | "multipart/form-data"
75 | ],
76 | "produces": [
77 | "application/json"
78 | ],
79 | "summary": "Add Torrent File",
80 | "operationId": "add-torrent",
81 | "parameters": [
82 | {
83 | "type": "file",
84 | "description": "torrent file",
85 | "name": "torrent",
86 | "in": "formData",
87 | "required": true
88 | },
89 | {
90 | "type": "boolean",
91 | "description": "ignore if duplicate",
92 | "name": "ignore_duplicate",
93 | "in": "query"
94 | },
95 | {
96 | "type": "boolean",
97 | "description": "start downloading",
98 | "name": "download",
99 | "in": "query"
100 | }
101 | ],
102 | "responses": {
103 | "200": {
104 | "description": "OK",
105 | "schema": {
106 | "$ref": "#/definitions/api.NewTorrentResponse"
107 | }
108 | },
109 | "400": {
110 | "description": "Bad Request",
111 | "schema": {
112 | "$ref": "#/definitions/api.ErrorResponse"
113 | }
114 | },
115 | "500": {
116 | "description": "Internal Server Error",
117 | "schema": {
118 | "$ref": "#/definitions/api.ErrorResponse"
119 | }
120 | }
121 | }
122 | }
123 | },
124 | "/pause": {
125 | "get": {
126 | "description": "pause service",
127 | "produces": [
128 | "application/json"
129 | ],
130 | "summary": "Pause",
131 | "operationId": "pause",
132 | "responses": {
133 | "200": {
134 | "description": "OK",
135 | "schema": {
136 | "$ref": "#/definitions/api.MessageResponse"
137 | }
138 | }
139 | }
140 | }
141 | },
142 | "/resume": {
143 | "get": {
144 | "description": "resume service",
145 | "produces": [
146 | "application/json"
147 | ],
148 | "summary": "Resume",
149 | "operationId": "resume",
150 | "responses": {
151 | "200": {
152 | "description": "OK",
153 | "schema": {
154 | "$ref": "#/definitions/api.MessageResponse"
155 | }
156 | }
157 | }
158 | }
159 | },
160 | "/settings/get": {
161 | "get": {
162 | "description": "get settings in JSON object",
163 | "produces": [
164 | "application/json"
165 | ],
166 | "summary": "Get current settings",
167 | "operationId": "get-settings",
168 | "responses": {
169 | "200": {
170 | "description": "OK",
171 | "schema": {
172 | "$ref": "#/definitions/settings.Settings"
173 | }
174 | }
175 | }
176 | }
177 | },
178 | "/settings/set": {
179 | "post": {
180 | "description": "set settings given the provided JSON object",
181 | "consumes": [
182 | "application/json"
183 | ],
184 | "produces": [
185 | "application/json"
186 | ],
187 | "summary": "Set settings",
188 | "operationId": "set-settings",
189 | "parameters": [
190 | {
191 | "description": "Settings to be set",
192 | "name": "default",
193 | "in": "body",
194 | "schema": {
195 | "$ref": "#/definitions/settings.Settings"
196 | }
197 | },
198 | {
199 | "type": "boolean",
200 | "description": "reset torrents",
201 | "name": "reset",
202 | "in": "query"
203 | }
204 | ],
205 | "responses": {
206 | "200": {
207 | "description": "OK",
208 | "schema": {
209 | "$ref": "#/definitions/settings.Settings"
210 | }
211 | },
212 | "500": {
213 | "description": "Internal Server Error",
214 | "schema": {
215 | "$ref": "#/definitions/api.ErrorResponse"
216 | }
217 | }
218 | }
219 | }
220 | },
221 | "/shutdown": {
222 | "get": {
223 | "description": "shutdown server",
224 | "summary": "Shutdown",
225 | "operationId": "shutdown",
226 | "responses": {
227 | "200": {
228 | "description": "OK"
229 | }
230 | }
231 | }
232 | },
233 | "/status": {
234 | "get": {
235 | "description": "get service status",
236 | "produces": [
237 | "application/json"
238 | ],
239 | "summary": "Status",
240 | "operationId": "status",
241 | "responses": {
242 | "200": {
243 | "description": "OK",
244 | "schema": {
245 | "$ref": "#/definitions/bittorrent.ServiceStatus"
246 | }
247 | }
248 | }
249 | }
250 | },
251 | "/torrents": {
252 | "get": {
253 | "description": "list all torrents from service",
254 | "produces": [
255 | "application/json"
256 | ],
257 | "summary": "List Torrents",
258 | "operationId": "list-torrents",
259 | "parameters": [
260 | {
261 | "type": "boolean",
262 | "description": "get torrents status",
263 | "name": "status",
264 | "in": "query"
265 | }
266 | ],
267 | "responses": {
268 | "200": {
269 | "description": "OK",
270 | "schema": {
271 | "type": "array",
272 | "items": {
273 | "$ref": "#/definitions/api.TorrentInfoResponse"
274 | }
275 | }
276 | }
277 | }
278 | }
279 | },
280 | "/torrents/{infoHash}/download": {
281 | "get": {
282 | "description": "download all files from torrent",
283 | "produces": [
284 | "application/json"
285 | ],
286 | "summary": "Download",
287 | "operationId": "download-torrent",
288 | "parameters": [
289 | {
290 | "type": "string",
291 | "description": "torrent info hash",
292 | "name": "infoHash",
293 | "in": "path",
294 | "required": true
295 | }
296 | ],
297 | "responses": {
298 | "200": {
299 | "description": "OK",
300 | "schema": {
301 | "$ref": "#/definitions/api.MessageResponse"
302 | }
303 | },
304 | "404": {
305 | "description": "Not Found",
306 | "schema": {
307 | "$ref": "#/definitions/api.ErrorResponse"
308 | }
309 | },
310 | "500": {
311 | "description": "Internal Server Error",
312 | "schema": {
313 | "$ref": "#/definitions/api.ErrorResponse"
314 | }
315 | }
316 | }
317 | }
318 | },
319 | "/torrents/{infoHash}/files": {
320 | "get": {
321 | "description": "get a list of the torrent files and its details",
322 | "produces": [
323 | "application/json"
324 | ],
325 | "summary": "Get Torrent Files",
326 | "operationId": "torrent-files",
327 | "parameters": [
328 | {
329 | "type": "string",
330 | "description": "torrent info hash",
331 | "name": "infoHash",
332 | "in": "path",
333 | "required": true
334 | },
335 | {
336 | "type": "boolean",
337 | "description": "get files status",
338 | "name": "status",
339 | "in": "query"
340 | }
341 | ],
342 | "responses": {
343 | "200": {
344 | "description": "OK",
345 | "schema": {
346 | "type": "array",
347 | "items": {
348 | "$ref": "#/definitions/api.FileInfoResponse"
349 | }
350 | }
351 | },
352 | "404": {
353 | "description": "Not Found",
354 | "schema": {
355 | "$ref": "#/definitions/api.ErrorResponse"
356 | }
357 | },
358 | "500": {
359 | "description": "Internal Server Error",
360 | "schema": {
361 | "$ref": "#/definitions/api.ErrorResponse"
362 | }
363 | }
364 | }
365 | }
366 | },
367 | "/torrents/{infoHash}/files/{file}/download": {
368 | "get": {
369 | "description": "download file from torrent given its id",
370 | "produces": [
371 | "application/json"
372 | ],
373 | "summary": "Download File",
374 | "operationId": "download-file",
375 | "parameters": [
376 | {
377 | "type": "string",
378 | "description": "torrent info hash",
379 | "name": "infoHash",
380 | "in": "path",
381 | "required": true
382 | },
383 | {
384 | "type": "integer",
385 | "description": "file id",
386 | "name": "file",
387 | "in": "path",
388 | "required": true
389 | },
390 | {
391 | "type": "boolean",
392 | "description": "buffer file",
393 | "name": "buffer",
394 | "in": "query"
395 | }
396 | ],
397 | "responses": {
398 | "200": {
399 | "description": "OK",
400 | "schema": {
401 | "$ref": "#/definitions/api.MessageResponse"
402 | }
403 | },
404 | "400": {
405 | "description": "Bad Request",
406 | "schema": {
407 | "$ref": "#/definitions/api.ErrorResponse"
408 | }
409 | },
410 | "404": {
411 | "description": "Not Found",
412 | "schema": {
413 | "$ref": "#/definitions/api.ErrorResponse"
414 | }
415 | }
416 | }
417 | }
418 | },
419 | "/torrents/{infoHash}/files/{file}/hash": {
420 | "get": {
421 | "description": "calculate file hash suitable for opensubtitles",
422 | "produces": [
423 | "application/json"
424 | ],
425 | "summary": "Calculate file hash",
426 | "operationId": "file-hash",
427 | "parameters": [
428 | {
429 | "type": "string",
430 | "description": "torrent info hash",
431 | "name": "infoHash",
432 | "in": "path",
433 | "required": true
434 | },
435 | {
436 | "type": "integer",
437 | "description": "file id",
438 | "name": "file",
439 | "in": "path",
440 | "required": true
441 | }
442 | ],
443 | "responses": {
444 | "200": {
445 | "description": "OK",
446 | "schema": {
447 | "$ref": "#/definitions/api.FileHash"
448 | }
449 | },
450 | "400": {
451 | "description": "Bad Request",
452 | "schema": {
453 | "$ref": "#/definitions/api.ErrorResponse"
454 | }
455 | },
456 | "404": {
457 | "description": "Not Found",
458 | "schema": {
459 | "$ref": "#/definitions/api.ErrorResponse"
460 | }
461 | },
462 | "500": {
463 | "description": "Internal Server Error",
464 | "schema": {
465 | "$ref": "#/definitions/api.ErrorResponse"
466 | }
467 | }
468 | }
469 | }
470 | },
471 | "/torrents/{infoHash}/files/{file}/info": {
472 | "get": {
473 | "description": "get file info from torrent given its id",
474 | "produces": [
475 | "application/json"
476 | ],
477 | "summary": "Get File Info",
478 | "operationId": "file-info",
479 | "parameters": [
480 | {
481 | "type": "string",
482 | "description": "torrent info hash",
483 | "name": "infoHash",
484 | "in": "path",
485 | "required": true
486 | },
487 | {
488 | "type": "integer",
489 | "description": "file id",
490 | "name": "file",
491 | "in": "path",
492 | "required": true
493 | }
494 | ],
495 | "responses": {
496 | "200": {
497 | "description": "OK",
498 | "schema": {
499 | "$ref": "#/definitions/bittorrent.FileInfo"
500 | }
501 | },
502 | "400": {
503 | "description": "Bad Request",
504 | "schema": {
505 | "$ref": "#/definitions/api.ErrorResponse"
506 | }
507 | },
508 | "404": {
509 | "description": "Not Found",
510 | "schema": {
511 | "$ref": "#/definitions/api.ErrorResponse"
512 | }
513 | }
514 | }
515 | }
516 | },
517 | "/torrents/{infoHash}/files/{file}/serve": {
518 | "get": {
519 | "description": "serve file from torrent given its id",
520 | "produces": [
521 | "application/json"
522 | ],
523 | "summary": "Serve File",
524 | "operationId": "serve-file",
525 | "parameters": [
526 | {
527 | "type": "string",
528 | "description": "torrent info hash",
529 | "name": "infoHash",
530 | "in": "path",
531 | "required": true
532 | },
533 | {
534 | "type": "integer",
535 | "description": "file id",
536 | "name": "file",
537 | "in": "path",
538 | "required": true
539 | }
540 | ],
541 | "responses": {
542 | "200": {},
543 | "400": {
544 | "description": "Bad Request",
545 | "schema": {
546 | "$ref": "#/definitions/api.ErrorResponse"
547 | }
548 | },
549 | "404": {
550 | "description": "Not Found",
551 | "schema": {
552 | "$ref": "#/definitions/api.ErrorResponse"
553 | }
554 | }
555 | }
556 | }
557 | },
558 | "/torrents/{infoHash}/files/{file}/status": {
559 | "get": {
560 | "description": "get file status from torrent given its id",
561 | "produces": [
562 | "application/json"
563 | ],
564 | "summary": "Get File Status",
565 | "operationId": "file-status",
566 | "parameters": [
567 | {
568 | "type": "string",
569 | "description": "torrent info hash",
570 | "name": "infoHash",
571 | "in": "path",
572 | "required": true
573 | },
574 | {
575 | "type": "integer",
576 | "description": "file id",
577 | "name": "file",
578 | "in": "path",
579 | "required": true
580 | }
581 | ],
582 | "responses": {
583 | "200": {
584 | "description": "OK",
585 | "schema": {
586 | "$ref": "#/definitions/bittorrent.FileStatus"
587 | }
588 | },
589 | "400": {
590 | "description": "Bad Request",
591 | "schema": {
592 | "$ref": "#/definitions/api.ErrorResponse"
593 | }
594 | },
595 | "404": {
596 | "description": "Not Found",
597 | "schema": {
598 | "$ref": "#/definitions/api.ErrorResponse"
599 | }
600 | }
601 | }
602 | }
603 | },
604 | "/torrents/{infoHash}/files/{file}/stop": {
605 | "get": {
606 | "description": "stop file download from torrent given its id",
607 | "produces": [
608 | "application/json"
609 | ],
610 | "summary": "Stop File Download",
611 | "operationId": "stop-file",
612 | "parameters": [
613 | {
614 | "type": "string",
615 | "description": "torrent info hash",
616 | "name": "infoHash",
617 | "in": "path",
618 | "required": true
619 | },
620 | {
621 | "type": "integer",
622 | "description": "file id",
623 | "name": "file",
624 | "in": "path",
625 | "required": true
626 | }
627 | ],
628 | "responses": {
629 | "200": {
630 | "description": "OK",
631 | "schema": {
632 | "$ref": "#/definitions/api.MessageResponse"
633 | }
634 | },
635 | "400": {
636 | "description": "Bad Request",
637 | "schema": {
638 | "$ref": "#/definitions/api.ErrorResponse"
639 | }
640 | },
641 | "404": {
642 | "description": "Not Found",
643 | "schema": {
644 | "$ref": "#/definitions/api.ErrorResponse"
645 | }
646 | }
647 | }
648 | }
649 | },
650 | "/torrents/{infoHash}/info": {
651 | "get": {
652 | "description": "get torrent info",
653 | "produces": [
654 | "application/json"
655 | ],
656 | "summary": "Get Torrent Info",
657 | "operationId": "torrent-info",
658 | "parameters": [
659 | {
660 | "type": "string",
661 | "description": "torrent info hash",
662 | "name": "infoHash",
663 | "in": "path",
664 | "required": true
665 | }
666 | ],
667 | "responses": {
668 | "200": {
669 | "description": "OK",
670 | "schema": {
671 | "$ref": "#/definitions/bittorrent.TorrentInfo"
672 | }
673 | },
674 | "404": {
675 | "description": "Not Found",
676 | "schema": {
677 | "$ref": "#/definitions/api.ErrorResponse"
678 | }
679 | }
680 | }
681 | }
682 | },
683 | "/torrents/{infoHash}/pause": {
684 | "get": {
685 | "description": "pause torrent from service",
686 | "produces": [
687 | "application/json"
688 | ],
689 | "summary": "Pause Torrent",
690 | "operationId": "pause-torrent",
691 | "parameters": [
692 | {
693 | "type": "string",
694 | "description": "torrent info hash",
695 | "name": "infoHash",
696 | "in": "path",
697 | "required": true
698 | }
699 | ],
700 | "responses": {
701 | "200": {
702 | "description": "OK",
703 | "schema": {
704 | "$ref": "#/definitions/api.MessageResponse"
705 | }
706 | },
707 | "404": {
708 | "description": "Not Found",
709 | "schema": {
710 | "$ref": "#/definitions/api.ErrorResponse"
711 | }
712 | }
713 | }
714 | }
715 | },
716 | "/torrents/{infoHash}/remove": {
717 | "get": {
718 | "description": "remove torrent from service",
719 | "produces": [
720 | "application/json"
721 | ],
722 | "summary": "Remove Torrent",
723 | "operationId": "remove-torrent",
724 | "parameters": [
725 | {
726 | "type": "string",
727 | "description": "torrent info hash",
728 | "name": "infoHash",
729 | "in": "path",
730 | "required": true
731 | },
732 | {
733 | "type": "boolean",
734 | "description": "delete files",
735 | "name": "delete",
736 | "in": "query"
737 | }
738 | ],
739 | "responses": {
740 | "200": {
741 | "description": "OK",
742 | "schema": {
743 | "$ref": "#/definitions/api.MessageResponse"
744 | }
745 | },
746 | "404": {
747 | "description": "Not Found",
748 | "schema": {
749 | "$ref": "#/definitions/api.ErrorResponse"
750 | }
751 | }
752 | }
753 | }
754 | },
755 | "/torrents/{infoHash}/resume": {
756 | "get": {
757 | "description": "resume a paused torrent",
758 | "produces": [
759 | "application/json"
760 | ],
761 | "summary": "Resume Torrent",
762 | "operationId": "resume-torrent",
763 | "parameters": [
764 | {
765 | "type": "string",
766 | "description": "torrent info hash",
767 | "name": "infoHash",
768 | "in": "path",
769 | "required": true
770 | }
771 | ],
772 | "responses": {
773 | "200": {
774 | "description": "OK",
775 | "schema": {
776 | "$ref": "#/definitions/api.MessageResponse"
777 | }
778 | },
779 | "404": {
780 | "description": "Not Found",
781 | "schema": {
782 | "$ref": "#/definitions/api.ErrorResponse"
783 | }
784 | }
785 | }
786 | }
787 | },
788 | "/torrents/{infoHash}/status": {
789 | "get": {
790 | "description": "get torrent status",
791 | "produces": [
792 | "application/json"
793 | ],
794 | "summary": "Get Torrent Status",
795 | "operationId": "torrent-status",
796 | "parameters": [
797 | {
798 | "type": "string",
799 | "description": "torrent info hash",
800 | "name": "infoHash",
801 | "in": "path",
802 | "required": true
803 | }
804 | ],
805 | "responses": {
806 | "200": {
807 | "description": "OK",
808 | "schema": {
809 | "$ref": "#/definitions/bittorrent.TorrentStatus"
810 | }
811 | },
812 | "404": {
813 | "description": "Not Found",
814 | "schema": {
815 | "$ref": "#/definitions/api.ErrorResponse"
816 | }
817 | }
818 | }
819 | }
820 | },
821 | "/torrents/{infoHash}/stop": {
822 | "get": {
823 | "description": "stop downloading torrent",
824 | "produces": [
825 | "application/json"
826 | ],
827 | "summary": "Stop Download",
828 | "operationId": "stop-torrent",
829 | "parameters": [
830 | {
831 | "type": "string",
832 | "description": "torrent info hash",
833 | "name": "infoHash",
834 | "in": "path",
835 | "required": true
836 | }
837 | ],
838 | "responses": {
839 | "200": {
840 | "description": "OK",
841 | "schema": {
842 | "$ref": "#/definitions/api.MessageResponse"
843 | }
844 | },
845 | "404": {
846 | "description": "Not Found",
847 | "schema": {
848 | "$ref": "#/definitions/api.ErrorResponse"
849 | }
850 | },
851 | "500": {
852 | "description": "Internal Server Error",
853 | "schema": {
854 | "$ref": "#/definitions/api.ErrorResponse"
855 | }
856 | }
857 | }
858 | }
859 | }
860 | },
861 | "definitions": {
862 | "api.ErrorResponse": {
863 | "type": "object",
864 | "properties": {
865 | "error": {
866 | "type": "string",
867 | "example": "Houston, we have a problem!"
868 | }
869 | }
870 | },
871 | "api.FileHash": {
872 | "type": "object",
873 | "properties": {
874 | "hash": {
875 | "type": "string"
876 | }
877 | }
878 | },
879 | "api.FileInfoResponse": {
880 | "type": "object",
881 | "properties": {
882 | "id": {
883 | "type": "integer"
884 | },
885 | "length": {
886 | "type": "integer"
887 | },
888 | "name": {
889 | "type": "string"
890 | },
891 | "path": {
892 | "type": "string"
893 | },
894 | "status": {
895 | "type": "object",
896 | "$ref": "#/definitions/bittorrent.FileStatus"
897 | }
898 | }
899 | },
900 | "api.MessageResponse": {
901 | "type": "object",
902 | "properties": {
903 | "message": {
904 | "type": "string",
905 | "example": "done"
906 | }
907 | }
908 | },
909 | "api.NewTorrentResponse": {
910 | "type": "object",
911 | "properties": {
912 | "info_hash": {
913 | "type": "string",
914 | "example": "000102030405060708090a0b0c0d0e0f10111213"
915 | }
916 | }
917 | },
918 | "api.TorrentInfoResponse": {
919 | "type": "object",
920 | "properties": {
921 | "info_hash": {
922 | "type": "string"
923 | },
924 | "name": {
925 | "type": "string"
926 | },
927 | "size": {
928 | "type": "integer"
929 | },
930 | "status": {
931 | "type": "object",
932 | "$ref": "#/definitions/bittorrent.TorrentStatus"
933 | }
934 | }
935 | },
936 | "bittorrent.FileInfo": {
937 | "type": "object",
938 | "properties": {
939 | "id": {
940 | "type": "integer"
941 | },
942 | "length": {
943 | "type": "integer"
944 | },
945 | "name": {
946 | "type": "string"
947 | },
948 | "path": {
949 | "type": "string"
950 | }
951 | }
952 | },
953 | "bittorrent.FileStatus": {
954 | "type": "object",
955 | "properties": {
956 | "buffering_progress": {
957 | "type": "number"
958 | },
959 | "buffering_total": {
960 | "type": "integer"
961 | },
962 | "priority": {
963 | "type": "integer"
964 | },
965 | "progress": {
966 | "type": "number"
967 | },
968 | "state": {
969 | "type": "integer"
970 | },
971 | "total": {
972 | "type": "integer"
973 | },
974 | "total_done": {
975 | "type": "integer"
976 | }
977 | }
978 | },
979 | "bittorrent.ServiceStatus": {
980 | "type": "object",
981 | "properties": {
982 | "download_rate": {
983 | "type": "integer"
984 | },
985 | "is_paused": {
986 | "type": "boolean"
987 | },
988 | "num_torrents": {
989 | "type": "integer"
990 | },
991 | "progress": {
992 | "type": "number"
993 | },
994 | "upload_rate": {
995 | "type": "integer"
996 | }
997 | }
998 | },
999 | "bittorrent.TorrentInfo": {
1000 | "type": "object",
1001 | "properties": {
1002 | "info_hash": {
1003 | "type": "string"
1004 | },
1005 | "name": {
1006 | "type": "string"
1007 | },
1008 | "size": {
1009 | "type": "integer"
1010 | }
1011 | }
1012 | },
1013 | "bittorrent.TorrentStatus": {
1014 | "type": "object",
1015 | "properties": {
1016 | "active_time": {
1017 | "type": "integer"
1018 | },
1019 | "all_time_download": {
1020 | "type": "integer"
1021 | },
1022 | "all_time_upload": {
1023 | "type": "integer"
1024 | },
1025 | "download_rate": {
1026 | "type": "integer"
1027 | },
1028 | "finished_time": {
1029 | "type": "integer"
1030 | },
1031 | "has_metadata": {
1032 | "type": "boolean"
1033 | },
1034 | "paused": {
1035 | "type": "boolean"
1036 | },
1037 | "peers": {
1038 | "type": "integer"
1039 | },
1040 | "peers_total": {
1041 | "type": "integer"
1042 | },
1043 | "progress": {
1044 | "type": "number"
1045 | },
1046 | "seeders": {
1047 | "type": "integer"
1048 | },
1049 | "seeders_total": {
1050 | "type": "integer"
1051 | },
1052 | "seeding_time": {
1053 | "type": "integer"
1054 | },
1055 | "state": {
1056 | "type": "integer"
1057 | },
1058 | "total": {
1059 | "type": "integer"
1060 | },
1061 | "total_done": {
1062 | "type": "integer"
1063 | },
1064 | "total_wanted": {
1065 | "type": "integer"
1066 | },
1067 | "total_wanted_done": {
1068 | "type": "integer"
1069 | },
1070 | "upload_rate": {
1071 | "type": "integer"
1072 | }
1073 | }
1074 | },
1075 | "settings.ProxySettings": {
1076 | "type": "object",
1077 | "properties": {
1078 | "hostname": {
1079 | "type": "string"
1080 | },
1081 | "password": {
1082 | "type": "string"
1083 | },
1084 | "port": {
1085 | "type": "integer"
1086 | },
1087 | "type": {
1088 | "type": "integer"
1089 | },
1090 | "username": {
1091 | "type": "string"
1092 | }
1093 | }
1094 | },
1095 | "settings.Settings": {
1096 | "type": "object",
1097 | "required": [
1098 | "download_path",
1099 | "torrents_path"
1100 | ],
1101 | "properties": {
1102 | "active_checking_limit": {
1103 | "type": "integer",
1104 | "example": 1
1105 | },
1106 | "active_dht_limit": {
1107 | "type": "integer",
1108 | "example": 88
1109 | },
1110 | "active_downloads_limit": {
1111 | "type": "integer",
1112 | "example": 3
1113 | },
1114 | "active_limit": {
1115 | "type": "integer",
1116 | "example": 500
1117 | },
1118 | "active_lsd_limit": {
1119 | "type": "integer",
1120 | "example": 60
1121 | },
1122 | "active_seeds_limit": {
1123 | "type": "integer",
1124 | "example": 5
1125 | },
1126 | "active_tracker_limit": {
1127 | "type": "integer",
1128 | "example": 1600
1129 | },
1130 | "alerts_log_level": {
1131 | "type": "integer",
1132 | "example": 0
1133 | },
1134 | "api_log_level": {
1135 | "type": "integer",
1136 | "example": 1
1137 | },
1138 | "buffer_size": {
1139 | "type": "integer",
1140 | "example": 20971520
1141 | },
1142 | "check_available_space": {
1143 | "type": "boolean",
1144 | "example": true
1145 | },
1146 | "connections_limit": {
1147 | "type": "integer",
1148 | "example": 200
1149 | },
1150 | "disable_dht": {
1151 | "type": "boolean",
1152 | "example": false
1153 | },
1154 | "disable_lsd": {
1155 | "type": "boolean",
1156 | "example": false
1157 | },
1158 | "disable_natpmp": {
1159 | "type": "boolean",
1160 | "example": false
1161 | },
1162 | "disable_upnp": {
1163 | "type": "boolean",
1164 | "example": false
1165 | },
1166 | "download_path": {
1167 | "type": "string",
1168 | "example": "downloads"
1169 | },
1170 | "encryption_policy": {
1171 | "type": "integer",
1172 | "example": 0
1173 | },
1174 | "limit_after_buffering": {
1175 | "type": "boolean",
1176 | "example": false
1177 | },
1178 | "listen_interfaces": {
1179 | "type": "string"
1180 | },
1181 | "listen_port": {
1182 | "type": "integer",
1183 | "example": 6889
1184 | },
1185 | "max_download_rate": {
1186 | "type": "integer",
1187 | "example": 0
1188 | },
1189 | "max_upload_rate": {
1190 | "type": "integer",
1191 | "example": 0
1192 | },
1193 | "outgoing_interfaces": {
1194 | "type": "string"
1195 | },
1196 | "piece_wait_timeout": {
1197 | "type": "integer",
1198 | "example": 60
1199 | },
1200 | "proxy": {
1201 | "type": "object",
1202 | "$ref": "#/definitions/settings.ProxySettings"
1203 | },
1204 | "seed_time_limit": {
1205 | "type": "integer",
1206 | "example": 86400
1207 | },
1208 | "seed_time_ratio_limit": {
1209 | "type": "integer",
1210 | "example": 700
1211 | },
1212 | "service_log_level": {
1213 | "type": "integer",
1214 | "example": 4
1215 | },
1216 | "session_save": {
1217 | "type": "integer",
1218 | "example": 30
1219 | },
1220 | "share_ratio_limit": {
1221 | "type": "integer",
1222 | "example": 200
1223 | },
1224 | "torrents_path": {
1225 | "type": "string",
1226 | "example": "downloads/torrents"
1227 | },
1228 | "tuned_storage": {
1229 | "type": "boolean",
1230 | "example": false
1231 | },
1232 | "user_agent": {
1233 | "type": "integer",
1234 | "example": 0
1235 | }
1236 | }
1237 | }
1238 | }
1239 | }
--------------------------------------------------------------------------------
/docs/swagger.yaml:
--------------------------------------------------------------------------------
1 | basePath: /
2 | definitions:
3 | api.ErrorResponse:
4 | properties:
5 | error:
6 | example: Houston, we have a problem!
7 | type: string
8 | type: object
9 | api.FileHash:
10 | properties:
11 | hash:
12 | type: string
13 | type: object
14 | api.FileInfoResponse:
15 | properties:
16 | id:
17 | type: integer
18 | length:
19 | type: integer
20 | name:
21 | type: string
22 | path:
23 | type: string
24 | status:
25 | $ref: '#/definitions/bittorrent.FileStatus'
26 | type: object
27 | type: object
28 | api.MessageResponse:
29 | properties:
30 | message:
31 | example: done
32 | type: string
33 | type: object
34 | api.NewTorrentResponse:
35 | properties:
36 | info_hash:
37 | example: 000102030405060708090a0b0c0d0e0f10111213
38 | type: string
39 | type: object
40 | api.TorrentInfoResponse:
41 | properties:
42 | info_hash:
43 | type: string
44 | name:
45 | type: string
46 | size:
47 | type: integer
48 | status:
49 | $ref: '#/definitions/bittorrent.TorrentStatus'
50 | type: object
51 | type: object
52 | bittorrent.FileInfo:
53 | properties:
54 | id:
55 | type: integer
56 | length:
57 | type: integer
58 | name:
59 | type: string
60 | path:
61 | type: string
62 | type: object
63 | bittorrent.FileStatus:
64 | properties:
65 | buffering_progress:
66 | type: number
67 | buffering_total:
68 | type: integer
69 | priority:
70 | type: integer
71 | progress:
72 | type: number
73 | state:
74 | type: integer
75 | total:
76 | type: integer
77 | total_done:
78 | type: integer
79 | type: object
80 | bittorrent.ServiceStatus:
81 | properties:
82 | download_rate:
83 | type: integer
84 | is_paused:
85 | type: boolean
86 | num_torrents:
87 | type: integer
88 | progress:
89 | type: number
90 | upload_rate:
91 | type: integer
92 | type: object
93 | bittorrent.TorrentInfo:
94 | properties:
95 | info_hash:
96 | type: string
97 | name:
98 | type: string
99 | size:
100 | type: integer
101 | type: object
102 | bittorrent.TorrentStatus:
103 | properties:
104 | active_time:
105 | type: integer
106 | all_time_download:
107 | type: integer
108 | all_time_upload:
109 | type: integer
110 | download_rate:
111 | type: integer
112 | finished_time:
113 | type: integer
114 | has_metadata:
115 | type: boolean
116 | paused:
117 | type: boolean
118 | peers:
119 | type: integer
120 | peers_total:
121 | type: integer
122 | progress:
123 | type: number
124 | seeders:
125 | type: integer
126 | seeders_total:
127 | type: integer
128 | seeding_time:
129 | type: integer
130 | state:
131 | type: integer
132 | total:
133 | type: integer
134 | total_done:
135 | type: integer
136 | total_wanted:
137 | type: integer
138 | total_wanted_done:
139 | type: integer
140 | upload_rate:
141 | type: integer
142 | type: object
143 | settings.ProxySettings:
144 | properties:
145 | hostname:
146 | type: string
147 | password:
148 | type: string
149 | port:
150 | type: integer
151 | type:
152 | type: integer
153 | username:
154 | type: string
155 | type: object
156 | settings.Settings:
157 | properties:
158 | active_checking_limit:
159 | example: 1
160 | type: integer
161 | active_dht_limit:
162 | example: 88
163 | type: integer
164 | active_downloads_limit:
165 | example: 3
166 | type: integer
167 | active_limit:
168 | example: 500
169 | type: integer
170 | active_lsd_limit:
171 | example: 60
172 | type: integer
173 | active_seeds_limit:
174 | example: 5
175 | type: integer
176 | active_tracker_limit:
177 | example: 1600
178 | type: integer
179 | alerts_log_level:
180 | example: 0
181 | type: integer
182 | api_log_level:
183 | example: 1
184 | type: integer
185 | buffer_size:
186 | example: 20971520
187 | type: integer
188 | check_available_space:
189 | example: true
190 | type: boolean
191 | connections_limit:
192 | example: 200
193 | type: integer
194 | disable_dht:
195 | example: false
196 | type: boolean
197 | disable_lsd:
198 | example: false
199 | type: boolean
200 | disable_natpmp:
201 | example: false
202 | type: boolean
203 | disable_upnp:
204 | example: false
205 | type: boolean
206 | download_path:
207 | example: downloads
208 | type: string
209 | encryption_policy:
210 | example: 0
211 | type: integer
212 | limit_after_buffering:
213 | example: false
214 | type: boolean
215 | listen_interfaces:
216 | type: string
217 | listen_port:
218 | example: 6889
219 | type: integer
220 | max_download_rate:
221 | example: 0
222 | type: integer
223 | max_upload_rate:
224 | example: 0
225 | type: integer
226 | outgoing_interfaces:
227 | type: string
228 | piece_wait_timeout:
229 | example: 60
230 | type: integer
231 | proxy:
232 | $ref: '#/definitions/settings.ProxySettings'
233 | type: object
234 | seed_time_limit:
235 | example: 86400
236 | type: integer
237 | seed_time_ratio_limit:
238 | example: 700
239 | type: integer
240 | service_log_level:
241 | example: 4
242 | type: integer
243 | session_save:
244 | example: 30
245 | type: integer
246 | share_ratio_limit:
247 | example: 200
248 | type: integer
249 | torrents_path:
250 | example: downloads/torrents
251 | type: string
252 | tuned_storage:
253 | example: false
254 | type: boolean
255 | user_agent:
256 | example: 0
257 | type: integer
258 | required:
259 | - download_path
260 | - torrents_path
261 | type: object
262 | info:
263 | contact:
264 | email: i96751414@gmail.com
265 | name: i96751414
266 | url: https://github.com/i96751414/torrest
267 | description: Torrent server with a REST API
268 | license:
269 | name: MIT
270 | url: https://github.com/i96751414/torrest/blob/master/LICENSE
271 | title: Torrest API
272 | version: "1.0"
273 | paths:
274 | /add/magnet:
275 | get:
276 | description: add magnet to service
277 | operationId: add-magnet
278 | parameters:
279 | - description: magnet URI
280 | in: query
281 | name: uri
282 | required: true
283 | type: string
284 | - description: ignore if duplicate
285 | in: query
286 | name: ignore_duplicate
287 | type: boolean
288 | - description: start downloading
289 | in: query
290 | name: download
291 | type: boolean
292 | produces:
293 | - application/json
294 | responses:
295 | "200":
296 | description: OK
297 | schema:
298 | $ref: '#/definitions/api.NewTorrentResponse'
299 | "400":
300 | description: Bad Request
301 | schema:
302 | $ref: '#/definitions/api.ErrorResponse'
303 | "500":
304 | description: Internal Server Error
305 | schema:
306 | $ref: '#/definitions/api.ErrorResponse'
307 | summary: Add Magnet
308 | /add/torrent:
309 | post:
310 | consumes:
311 | - multipart/form-data
312 | description: add torrent file to service
313 | operationId: add-torrent
314 | parameters:
315 | - description: torrent file
316 | in: formData
317 | name: torrent
318 | required: true
319 | type: file
320 | - description: ignore if duplicate
321 | in: query
322 | name: ignore_duplicate
323 | type: boolean
324 | - description: start downloading
325 | in: query
326 | name: download
327 | type: boolean
328 | produces:
329 | - application/json
330 | responses:
331 | "200":
332 | description: OK
333 | schema:
334 | $ref: '#/definitions/api.NewTorrentResponse'
335 | "400":
336 | description: Bad Request
337 | schema:
338 | $ref: '#/definitions/api.ErrorResponse'
339 | "500":
340 | description: Internal Server Error
341 | schema:
342 | $ref: '#/definitions/api.ErrorResponse'
343 | summary: Add Torrent File
344 | /pause:
345 | get:
346 | description: pause service
347 | operationId: pause
348 | produces:
349 | - application/json
350 | responses:
351 | "200":
352 | description: OK
353 | schema:
354 | $ref: '#/definitions/api.MessageResponse'
355 | summary: Pause
356 | /resume:
357 | get:
358 | description: resume service
359 | operationId: resume
360 | produces:
361 | - application/json
362 | responses:
363 | "200":
364 | description: OK
365 | schema:
366 | $ref: '#/definitions/api.MessageResponse'
367 | summary: Resume
368 | /settings/get:
369 | get:
370 | description: get settings in JSON object
371 | operationId: get-settings
372 | produces:
373 | - application/json
374 | responses:
375 | "200":
376 | description: OK
377 | schema:
378 | $ref: '#/definitions/settings.Settings'
379 | summary: Get current settings
380 | /settings/set:
381 | post:
382 | consumes:
383 | - application/json
384 | description: set settings given the provided JSON object
385 | operationId: set-settings
386 | parameters:
387 | - description: Settings to be set
388 | in: body
389 | name: default
390 | schema:
391 | $ref: '#/definitions/settings.Settings'
392 | - description: reset torrents
393 | in: query
394 | name: reset
395 | type: boolean
396 | produces:
397 | - application/json
398 | responses:
399 | "200":
400 | description: OK
401 | schema:
402 | $ref: '#/definitions/settings.Settings'
403 | "500":
404 | description: Internal Server Error
405 | schema:
406 | $ref: '#/definitions/api.ErrorResponse'
407 | summary: Set settings
408 | /shutdown:
409 | get:
410 | description: shutdown server
411 | operationId: shutdown
412 | responses:
413 | "200":
414 | description: OK
415 | summary: Shutdown
416 | /status:
417 | get:
418 | description: get service status
419 | operationId: status
420 | produces:
421 | - application/json
422 | responses:
423 | "200":
424 | description: OK
425 | schema:
426 | $ref: '#/definitions/bittorrent.ServiceStatus'
427 | summary: Status
428 | /torrents:
429 | get:
430 | description: list all torrents from service
431 | operationId: list-torrents
432 | parameters:
433 | - description: get torrents status
434 | in: query
435 | name: status
436 | type: boolean
437 | produces:
438 | - application/json
439 | responses:
440 | "200":
441 | description: OK
442 | schema:
443 | items:
444 | $ref: '#/definitions/api.TorrentInfoResponse'
445 | type: array
446 | summary: List Torrents
447 | /torrents/{infoHash}/download:
448 | get:
449 | description: download all files from torrent
450 | operationId: download-torrent
451 | parameters:
452 | - description: torrent info hash
453 | in: path
454 | name: infoHash
455 | required: true
456 | type: string
457 | produces:
458 | - application/json
459 | responses:
460 | "200":
461 | description: OK
462 | schema:
463 | $ref: '#/definitions/api.MessageResponse'
464 | "404":
465 | description: Not Found
466 | schema:
467 | $ref: '#/definitions/api.ErrorResponse'
468 | "500":
469 | description: Internal Server Error
470 | schema:
471 | $ref: '#/definitions/api.ErrorResponse'
472 | summary: Download
473 | /torrents/{infoHash}/files:
474 | get:
475 | description: get a list of the torrent files and its details
476 | operationId: torrent-files
477 | parameters:
478 | - description: torrent info hash
479 | in: path
480 | name: infoHash
481 | required: true
482 | type: string
483 | - description: get files status
484 | in: query
485 | name: status
486 | type: boolean
487 | produces:
488 | - application/json
489 | responses:
490 | "200":
491 | description: OK
492 | schema:
493 | items:
494 | $ref: '#/definitions/api.FileInfoResponse'
495 | type: array
496 | "404":
497 | description: Not Found
498 | schema:
499 | $ref: '#/definitions/api.ErrorResponse'
500 | "500":
501 | description: Internal Server Error
502 | schema:
503 | $ref: '#/definitions/api.ErrorResponse'
504 | summary: Get Torrent Files
505 | /torrents/{infoHash}/files/{file}/download:
506 | get:
507 | description: download file from torrent given its id
508 | operationId: download-file
509 | parameters:
510 | - description: torrent info hash
511 | in: path
512 | name: infoHash
513 | required: true
514 | type: string
515 | - description: file id
516 | in: path
517 | name: file
518 | required: true
519 | type: integer
520 | - description: buffer file
521 | in: query
522 | name: buffer
523 | type: boolean
524 | produces:
525 | - application/json
526 | responses:
527 | "200":
528 | description: OK
529 | schema:
530 | $ref: '#/definitions/api.MessageResponse'
531 | "400":
532 | description: Bad Request
533 | schema:
534 | $ref: '#/definitions/api.ErrorResponse'
535 | "404":
536 | description: Not Found
537 | schema:
538 | $ref: '#/definitions/api.ErrorResponse'
539 | summary: Download File
540 | /torrents/{infoHash}/files/{file}/hash:
541 | get:
542 | description: calculate file hash suitable for opensubtitles
543 | operationId: file-hash
544 | parameters:
545 | - description: torrent info hash
546 | in: path
547 | name: infoHash
548 | required: true
549 | type: string
550 | - description: file id
551 | in: path
552 | name: file
553 | required: true
554 | type: integer
555 | produces:
556 | - application/json
557 | responses:
558 | "200":
559 | description: OK
560 | schema:
561 | $ref: '#/definitions/api.FileHash'
562 | "400":
563 | description: Bad Request
564 | schema:
565 | $ref: '#/definitions/api.ErrorResponse'
566 | "404":
567 | description: Not Found
568 | schema:
569 | $ref: '#/definitions/api.ErrorResponse'
570 | "500":
571 | description: Internal Server Error
572 | schema:
573 | $ref: '#/definitions/api.ErrorResponse'
574 | summary: Calculate file hash
575 | /torrents/{infoHash}/files/{file}/info:
576 | get:
577 | description: get file info from torrent given its id
578 | operationId: file-info
579 | parameters:
580 | - description: torrent info hash
581 | in: path
582 | name: infoHash
583 | required: true
584 | type: string
585 | - description: file id
586 | in: path
587 | name: file
588 | required: true
589 | type: integer
590 | produces:
591 | - application/json
592 | responses:
593 | "200":
594 | description: OK
595 | schema:
596 | $ref: '#/definitions/bittorrent.FileInfo'
597 | "400":
598 | description: Bad Request
599 | schema:
600 | $ref: '#/definitions/api.ErrorResponse'
601 | "404":
602 | description: Not Found
603 | schema:
604 | $ref: '#/definitions/api.ErrorResponse'
605 | summary: Get File Info
606 | /torrents/{infoHash}/files/{file}/serve:
607 | get:
608 | description: serve file from torrent given its id
609 | operationId: serve-file
610 | parameters:
611 | - description: torrent info hash
612 | in: path
613 | name: infoHash
614 | required: true
615 | type: string
616 | - description: file id
617 | in: path
618 | name: file
619 | required: true
620 | type: integer
621 | produces:
622 | - application/json
623 | responses:
624 | "200": {}
625 | "400":
626 | description: Bad Request
627 | schema:
628 | $ref: '#/definitions/api.ErrorResponse'
629 | "404":
630 | description: Not Found
631 | schema:
632 | $ref: '#/definitions/api.ErrorResponse'
633 | summary: Serve File
634 | /torrents/{infoHash}/files/{file}/status:
635 | get:
636 | description: get file status from torrent given its id
637 | operationId: file-status
638 | parameters:
639 | - description: torrent info hash
640 | in: path
641 | name: infoHash
642 | required: true
643 | type: string
644 | - description: file id
645 | in: path
646 | name: file
647 | required: true
648 | type: integer
649 | produces:
650 | - application/json
651 | responses:
652 | "200":
653 | description: OK
654 | schema:
655 | $ref: '#/definitions/bittorrent.FileStatus'
656 | "400":
657 | description: Bad Request
658 | schema:
659 | $ref: '#/definitions/api.ErrorResponse'
660 | "404":
661 | description: Not Found
662 | schema:
663 | $ref: '#/definitions/api.ErrorResponse'
664 | summary: Get File Status
665 | /torrents/{infoHash}/files/{file}/stop:
666 | get:
667 | description: stop file download from torrent given its id
668 | operationId: stop-file
669 | parameters:
670 | - description: torrent info hash
671 | in: path
672 | name: infoHash
673 | required: true
674 | type: string
675 | - description: file id
676 | in: path
677 | name: file
678 | required: true
679 | type: integer
680 | produces:
681 | - application/json
682 | responses:
683 | "200":
684 | description: OK
685 | schema:
686 | $ref: '#/definitions/api.MessageResponse'
687 | "400":
688 | description: Bad Request
689 | schema:
690 | $ref: '#/definitions/api.ErrorResponse'
691 | "404":
692 | description: Not Found
693 | schema:
694 | $ref: '#/definitions/api.ErrorResponse'
695 | summary: Stop File Download
696 | /torrents/{infoHash}/info:
697 | get:
698 | description: get torrent info
699 | operationId: torrent-info
700 | parameters:
701 | - description: torrent info hash
702 | in: path
703 | name: infoHash
704 | required: true
705 | type: string
706 | produces:
707 | - application/json
708 | responses:
709 | "200":
710 | description: OK
711 | schema:
712 | $ref: '#/definitions/bittorrent.TorrentInfo'
713 | "404":
714 | description: Not Found
715 | schema:
716 | $ref: '#/definitions/api.ErrorResponse'
717 | summary: Get Torrent Info
718 | /torrents/{infoHash}/pause:
719 | get:
720 | description: pause torrent from service
721 | operationId: pause-torrent
722 | parameters:
723 | - description: torrent info hash
724 | in: path
725 | name: infoHash
726 | required: true
727 | type: string
728 | produces:
729 | - application/json
730 | responses:
731 | "200":
732 | description: OK
733 | schema:
734 | $ref: '#/definitions/api.MessageResponse'
735 | "404":
736 | description: Not Found
737 | schema:
738 | $ref: '#/definitions/api.ErrorResponse'
739 | summary: Pause Torrent
740 | /torrents/{infoHash}/remove:
741 | get:
742 | description: remove torrent from service
743 | operationId: remove-torrent
744 | parameters:
745 | - description: torrent info hash
746 | in: path
747 | name: infoHash
748 | required: true
749 | type: string
750 | - description: delete files
751 | in: query
752 | name: delete
753 | type: boolean
754 | produces:
755 | - application/json
756 | responses:
757 | "200":
758 | description: OK
759 | schema:
760 | $ref: '#/definitions/api.MessageResponse'
761 | "404":
762 | description: Not Found
763 | schema:
764 | $ref: '#/definitions/api.ErrorResponse'
765 | summary: Remove Torrent
766 | /torrents/{infoHash}/resume:
767 | get:
768 | description: resume a paused torrent
769 | operationId: resume-torrent
770 | parameters:
771 | - description: torrent info hash
772 | in: path
773 | name: infoHash
774 | required: true
775 | type: string
776 | produces:
777 | - application/json
778 | responses:
779 | "200":
780 | description: OK
781 | schema:
782 | $ref: '#/definitions/api.MessageResponse'
783 | "404":
784 | description: Not Found
785 | schema:
786 | $ref: '#/definitions/api.ErrorResponse'
787 | summary: Resume Torrent
788 | /torrents/{infoHash}/status:
789 | get:
790 | description: get torrent status
791 | operationId: torrent-status
792 | parameters:
793 | - description: torrent info hash
794 | in: path
795 | name: infoHash
796 | required: true
797 | type: string
798 | produces:
799 | - application/json
800 | responses:
801 | "200":
802 | description: OK
803 | schema:
804 | $ref: '#/definitions/bittorrent.TorrentStatus'
805 | "404":
806 | description: Not Found
807 | schema:
808 | $ref: '#/definitions/api.ErrorResponse'
809 | summary: Get Torrent Status
810 | /torrents/{infoHash}/stop:
811 | get:
812 | description: stop downloading torrent
813 | operationId: stop-torrent
814 | parameters:
815 | - description: torrent info hash
816 | in: path
817 | name: infoHash
818 | required: true
819 | type: string
820 | produces:
821 | - application/json
822 | responses:
823 | "200":
824 | description: OK
825 | schema:
826 | $ref: '#/definitions/api.MessageResponse'
827 | "404":
828 | description: Not Found
829 | schema:
830 | $ref: '#/definitions/api.ErrorResponse'
831 | "500":
832 | description: Internal Server Error
833 | schema:
834 | $ref: '#/definitions/api.ErrorResponse'
835 | summary: Stop Download
836 | swagger: "2.0"
837 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/i96751414/torrest
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
7 | github.com/dustin/go-humanize v1.0.0
8 | github.com/gin-gonic/gin v1.7.4
9 | github.com/go-playground/validator v9.31.0+incompatible
10 | github.com/i96751414/libtorrent-go v1.2.14-0
11 | github.com/jinzhu/copier v0.3.2
12 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
13 | github.com/modern-go/reflect2 v1.0.1 // indirect
14 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
15 | github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2
16 | github.com/swaggo/gin-swagger v1.3.3
17 | github.com/swaggo/swag v1.7.4
18 | github.com/ugorji/go v1.1.13 // indirect
19 | github.com/zeebo/bencode v1.0.0
20 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
21 | )
22 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
3 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
4 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
5 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
6 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
7 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
8 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
9 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
10 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
11 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
15 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
16 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
17 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
18 | github.com/gin-contrib/gzip v0.0.3 h1:etUaeesHhEORpZMp18zoOhepboiWnFtXrBZxszWUn4k=
19 | github.com/gin-contrib/gzip v0.0.3/go.mod h1:YxxswVZIqOvcHEQpsSn+QF5guQtO1dCfy0shBPy4jFc=
20 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
21 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
22 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
23 | github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM=
24 | github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
25 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
26 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
27 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
28 | github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
29 | github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
30 | github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ=
31 | github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg=
32 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
33 | github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng=
34 | github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
35 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
36 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
37 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
38 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
39 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
40 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
41 | github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA=
42 | github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig=
43 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
44 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
45 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
46 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
47 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
48 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
49 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
50 | github.com/i96751414/libtorrent-go v1.2.14-0 h1:l+Qh2FgZCh4fEHaNdMMp7Ut7It4i5SxRUgdV7ybBYok=
51 | github.com/i96751414/libtorrent-go v1.2.14-0/go.mod h1:9g0NJi4GtK3R3hn8hNDzjjNq4nMEtwAOIBzORVyMBas=
52 | github.com/jinzhu/copier v0.3.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w=
53 | github.com/jinzhu/copier v0.3.2/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro=
54 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
55 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
56 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
57 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
58 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
59 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
60 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
61 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
62 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
63 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
64 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
65 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
66 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
67 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
68 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
69 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
70 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
71 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
72 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
73 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
74 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
75 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
76 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
77 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
78 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
79 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
80 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
81 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
82 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
83 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
84 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
85 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
86 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
87 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
88 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
89 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
90 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
91 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
92 | github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
93 | github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 h1:+iNTcqQJy0OZ5jk6a5NLib47eqXK8uYcPX+O4+cBpEM=
94 | github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
95 | github.com/swaggo/gin-swagger v1.3.3 h1:XHyYmeNVFG5PbyWHG4jXtxOm2P4kiZapDCWsyDDiQ/I=
96 | github.com/swaggo/gin-swagger v1.3.3/go.mod h1:ymsZuGpbbu+S7ZoQ49QPpZoDBj6uqhb8WizgQPVgWl0=
97 | github.com/swaggo/swag v1.7.4 h1:up+ixy8yOqJKiFcuhMgkuYuF4xnevuhnFAXXF8OSfNg=
98 | github.com/swaggo/swag v1.7.4/go.mod h1:zD8h6h4SPv7t3l+4BKdRquqW1ASWjKZgT6Qv9z3kNqI=
99 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
100 | github.com/ugorji/go v1.1.13 h1:nB3O5kBSQGjEQAcfe1aLUYuxmXdFKmYgBZhY32rQb6Q=
101 | github.com/ugorji/go v1.1.13/go.mod h1:jxau1n+/wyTGLQoCkjok9r5zFa/FxT6eI5HiHKQszjc=
102 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
103 | github.com/ugorji/go/codec v1.1.13 h1:013LbFhocBoIqgHeIHKlV4JWYhqogATYWZhIcH0WHn4=
104 | github.com/ugorji/go/codec v1.1.13/go.mod h1:oNVt3Dq+FO91WNQ/9JnHKQP2QJxTzoN7wCBFCq1OeuU=
105 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
106 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
107 | github.com/zeebo/bencode v1.0.0 h1:zgop0Wu1nu4IexAZeCZ5qbsjU4O1vMrfCrVgUjbHVuA=
108 | github.com/zeebo/bencode v1.0.0/go.mod h1:Ct7CkrWIQuLWAy9M3atFHYq4kG9Ao/SsY5cdtCXmp9Y=
109 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
110 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
111 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
112 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
113 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
114 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
115 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
116 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
117 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
118 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
119 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
120 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
121 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
122 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
123 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
124 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
125 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
126 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
127 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
128 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
129 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
130 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
131 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
132 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
133 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
134 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
135 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
136 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
137 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
138 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
139 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
140 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
141 | golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
142 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
143 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
144 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
145 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
146 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
147 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
148 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
149 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
150 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
151 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
152 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
153 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
154 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
155 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
156 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
157 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
158 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
159 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
160 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
161 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "net/http"
7 | "os"
8 | "os/signal"
9 | "runtime"
10 | "strconv"
11 | "syscall"
12 |
13 | "github.com/i96751414/torrest/api"
14 | "github.com/i96751414/torrest/bittorrent"
15 | "github.com/i96751414/torrest/settings"
16 | "github.com/op/go-logging"
17 | )
18 |
19 | var log = logging.MustGetLogger("main")
20 |
21 | func main() {
22 | // Parse necessary arguments
23 | var listenPort int
24 | var settingsPath, origin string
25 | flag.IntVar(&listenPort, "port", 8080, "Server listen port")
26 | flag.StringVar(&settingsPath, "settings", "settings.json", "Settings path")
27 | flag.StringVar(&origin, "origin", "*", "Access-Control-Allow-Origin header value")
28 | flag.Parse()
29 |
30 | // Make sure we are properly multi threaded.
31 | runtime.GOMAXPROCS(runtime.NumCPU())
32 |
33 | logging.SetFormatter(logging.MustStringFormatter(
34 | `%{color}%{time:2006-01-02 15:04:05.000} %{level:-8s} [%{module}] %{shortfunc} - %{message}%{color:reset}`,
35 | ))
36 | logging.SetBackend(logging.NewLogBackend(os.Stdout, "", 0))
37 |
38 | m := http.NewServeMux()
39 | s := http.Server{
40 | Addr: ":" + strconv.Itoa(listenPort),
41 | Handler: m,
42 | }
43 |
44 | ctx, cancel := context.WithCancel(context.Background())
45 | defer cancel()
46 |
47 | log.Info("Loading configs")
48 | config, err := settings.Load(settingsPath)
49 | if err != nil {
50 | log.Fatalf("Failed loading settings: %s", err)
51 | }
52 |
53 | log.Info("Starting bittorrent service")
54 | service := bittorrent.NewService(config)
55 | defer service.Close()
56 |
57 | m.Handle("/", api.Routes(config, service, origin))
58 | m.HandleFunc("/shutdown", shutdown(cancel, origin))
59 |
60 | log.Infof("Starting torrent daemon on port %d", listenPort)
61 | go func() {
62 | if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
63 | log.Fatal(err)
64 | }
65 | }()
66 |
67 | quit := make(chan os.Signal)
68 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
69 |
70 | select {
71 | case <-ctx.Done():
72 | case <-quit:
73 | }
74 |
75 | log.Info("Shutting down daemon")
76 | if err := s.Shutdown(ctx); err != nil && err != context.Canceled {
77 | log.Errorf("Failed shutting down http server gracefully: %s", err)
78 | }
79 | }
80 |
81 | // @Summary Shutdown
82 | // @Description shutdown server
83 | // @ID shutdown
84 | // @Success 200 "OK"
85 | // @Router /shutdown [get]
86 | func shutdown(cancel context.CancelFunc, origin string) func(w http.ResponseWriter, r *http.Request) {
87 | return func(w http.ResponseWriter, r *http.Request) {
88 | switch r.Method {
89 | case http.MethodGet:
90 | cancel()
91 | w.Header().Set("Access-Control-Allow-Origin", origin)
92 | default:
93 | w.WriteHeader(http.StatusNotFound)
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/platform_host.mk:
--------------------------------------------------------------------------------
1 | ifeq ($(OS), Windows_NT)
2 | HOST_OS = windows
3 | ifeq ($(PROCESSOR_ARCHITECTURE), AMD64)
4 | ARCH = x64
5 | else ifeq ($(PROCESSOR_ARCHITECTURE), x86)
6 | ARCH = x86
7 | endif
8 | else
9 | UNAME_S := $(shell uname -s)
10 | UNAME_M := $(shell uname -m)
11 | ifeq ($(UNAME_S), Linux)
12 | HOST_OS = linux
13 | else ifeq ($(UNAME_S), Darwin)
14 | HOST_OS = darwin
15 | endif
16 | ifeq ($(UNAME_M), x86_64)
17 | HOST_ARCH = x64
18 | else ifneq ($(filter %86, $(UNAME_M)),)
19 | HOST_ARCH = x86
20 | else ifneq ($(findstring arm, $(UNAME_M)),)
21 | HOST_ARCH = arm
22 | endif
23 | endif
24 |
--------------------------------------------------------------------------------
/platform_target.mk:
--------------------------------------------------------------------------------
1 | GCC_TARGET = $(CC)
2 |
3 | ifneq ($(findstring darwin, $(GCC_TARGET)),)
4 | TARGET_OS = darwin
5 | else ifneq ($(findstring mingw, $(GCC_TARGET)),)
6 | TARGET_OS = windows
7 | else ifneq ($(findstring android, $(GCC_TARGET)),)
8 | TARGET_OS = android
9 | else ifneq ($(findstring linux, $(GCC_TARGET)),)
10 | TARGET_OS = linux
11 | endif
12 |
13 | ifneq ($(findstring x86_64, $(GCC_TARGET)),)
14 | TARGET_ARCH = x64
15 | else ifneq ($(findstring i386, $(GCC_TARGET)),)
16 | TARGET_ARCH = x86
17 | else ifneq ($(findstring i486, $(GCC_TARGET)),)
18 | TARGET_ARCH = x86
19 | else ifneq ($(findstring i586, $(GCC_TARGET)),)
20 | TARGET_ARCH = x86
21 | else ifneq ($(findstring i686, $(GCC_TARGET)),)
22 | TARGET_ARCH = x86
23 | else ifneq ($(findstring aarch64, $(GCC_TARGET)),)
24 | TARGET_ARCH = arm64
25 | else ifneq ($(findstring armv7, $(GCC_TARGET)),)
26 | TARGET_ARCH = armv7
27 | else ifneq ($(findstring arm, $(GCC_TARGET)),)
28 | TARGET_ARCH = arm
29 | endif
30 |
--------------------------------------------------------------------------------
/settings/settings.go:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "path/filepath"
7 | "time"
8 |
9 | "github.com/go-playground/validator"
10 | "github.com/jinzhu/copier"
11 | "github.com/op/go-logging"
12 | )
13 |
14 | var validate = validator.New()
15 |
16 | type UserAgentType int
17 |
18 | //noinspection GoSnakeCaseUsage
19 | const (
20 | DefaultUA UserAgentType = iota
21 | LibtorrentUA
22 | LibtorrentRasterbar_1_1_0_UA
23 | BitTorrent_7_5_0_UA
24 | BitTorrent_7_4_3_UA
25 | UTorrent_3_4_9_UA
26 | UTorrent_3_2_0_UA
27 | UTorrent_2_2_1_UA
28 | Transmission_2_92_UA
29 | Deluge_1_3_6_0_UA
30 | Deluge_1_3_12_0_UA
31 | Vuze_5_7_3_0_UA
32 | )
33 |
34 | type EncryptionPolicy int
35 |
36 | const (
37 | EncryptionEnabledPolicy EncryptionPolicy = iota
38 | EncryptionDisabledPolicy
39 | EncryptionForcedPolicy
40 | )
41 |
42 | type ProxyType int
43 |
44 | //noinspection GoUnusedConst
45 | const (
46 | ProxyTypeNone ProxyType = iota
47 | ProxyTypeSocks4
48 | ProxyTypeSocks5
49 | ProxyTypeSocks5Password
50 | ProxyTypeSocksHTTP
51 | ProxyTypeSocksHTTPPassword
52 | ProxyTypeI2PSAM
53 | )
54 |
55 | type ProxySettings struct {
56 | Type ProxyType `json:"type" validate:"gte=0,lte=6"`
57 | Port int `json:"port" validate:"gte=0,lte=65535"`
58 | Hostname string `json:"hostname"`
59 | Username string `json:"username"`
60 | Password string `json:"password"`
61 | }
62 |
63 | // Settings define the server settings
64 | type Settings struct {
65 | settingsPath string `json:"-"`
66 |
67 | ListenPort uint `json:"listen_port" validate:"gte=0,lte=65535" example:"6889"`
68 | ListenInterfaces string `json:"listen_interfaces" example:""`
69 | OutgoingInterfaces string `json:"outgoing_interfaces" example:""`
70 | DisableDHT bool `json:"disable_dht" example:"false"`
71 | DisableUPNP bool `json:"disable_upnp" example:"false"`
72 | DisableNatPMP bool `json:"disable_natpmp" example:"false"`
73 | DisableLSD bool `json:"disable_lsd" example:"false"`
74 | DownloadPath string `json:"download_path" validate:"required" example:"downloads"`
75 | TorrentsPath string `json:"torrents_path" validate:"required" example:"downloads/torrents"`
76 | UserAgent UserAgentType `json:"user_agent" validate:"gte=0,lte=6" example:"0"`
77 | SessionSave time.Duration `json:"session_save" validate:"gt=0" example:"30" swaggertype:"integer"`
78 | TunedStorage bool `json:"tuned_storage" example:"false"`
79 | CheckAvailableSpace bool `json:"check_available_space" example:"true"`
80 | ConnectionsLimit int `json:"connections_limit" example:"200"`
81 | LimitAfterBuffering bool `json:"limit_after_buffering" example:"false"`
82 | MaxDownloadRate int `json:"max_download_rate" validate:"gte=0" example:"0"`
83 | MaxUploadRate int `json:"max_upload_rate" validate:"gte=0" example:"0"`
84 | ShareRatioLimit int `json:"share_ratio_limit" validate:"gte=0" example:"200"`
85 | SeedTimeRatioLimit int `json:"seed_time_ratio_limit" validate:"gte=0" example:"700"`
86 | SeedTimeLimit int `json:"seed_time_limit" validate:"gte=0" example:"86400"`
87 | ActiveDownloadsLimit int `json:"active_downloads_limit" example:"3"`
88 | ActiveSeedsLimit int `json:"active_seeds_limit" example:"5"`
89 | ActiveCheckingLimit int `json:"active_checking_limit" example:"1"`
90 | ActiveDhtLimit int `json:"active_dht_limit" example:"88"`
91 | ActiveTrackerLimit int `json:"active_tracker_limit" example:"1600"`
92 | ActiveLsdLimit int `json:"active_lsd_limit" example:"60"`
93 | ActiveLimit int `json:"active_limit" example:"500"`
94 | EncryptionPolicy EncryptionPolicy `json:"encryption_policy" validate:"gte=0,lte=2" example:"0"`
95 | Proxy *ProxySettings `json:"proxy"`
96 | BufferSize int64 `json:"buffer_size" example:"20971520"`
97 | PieceWaitTimeout time.Duration `json:"piece_wait_timeout" validate:"gte=0" example:"60" swaggertype:"integer"`
98 | ServiceLogLevel logging.Level `json:"service_log_level" validate:"gte=0,lte=5" example:"4" swaggertype:"integer"`
99 | AlertsLogLevel logging.Level `json:"alerts_log_level" validate:"gte=0,lte=5" example:"0" swaggertype:"integer"`
100 | ApiLogLevel logging.Level `json:"api_log_level" validate:"gte=0,lte=5" example:"1" swaggertype:"integer"`
101 | }
102 |
103 | func DefaultSettings() *Settings {
104 | return &Settings{
105 | settingsPath: "settings.json",
106 | ListenPort: 6889,
107 | ListenInterfaces: "",
108 | OutgoingInterfaces: "",
109 | DisableDHT: false,
110 | DisableUPNP: false,
111 | DisableNatPMP: false,
112 | DisableLSD: false,
113 | DownloadPath: "downloads",
114 | TorrentsPath: filepath.Join("downloads", "torrents"),
115 | UserAgent: DefaultUA,
116 | SessionSave: 30,
117 | TunedStorage: false,
118 | CheckAvailableSpace: true,
119 | ConnectionsLimit: 0,
120 | LimitAfterBuffering: false,
121 | MaxDownloadRate: 0,
122 | MaxUploadRate: 0,
123 | ShareRatioLimit: 0,
124 | SeedTimeRatioLimit: 0,
125 | SeedTimeLimit: 0,
126 | ActiveDownloadsLimit: 3,
127 | ActiveSeedsLimit: 5,
128 | ActiveCheckingLimit: 1,
129 | ActiveDhtLimit: 88,
130 | ActiveTrackerLimit: 1600,
131 | ActiveLsdLimit: 60,
132 | ActiveLimit: 500,
133 | EncryptionPolicy: EncryptionEnabledPolicy,
134 | Proxy: nil,
135 | BufferSize: 20 * 1024 * 1024,
136 | PieceWaitTimeout: 60,
137 | ServiceLogLevel: logging.INFO,
138 | AlertsLogLevel: logging.CRITICAL,
139 | ApiLogLevel: logging.ERROR,
140 | }
141 | }
142 |
143 | // Load loads settings from path
144 | func Load(path string) (s *Settings, err error) {
145 | s = DefaultSettings()
146 | s.SetSettingsPath(path)
147 |
148 | if data, e := ioutil.ReadFile(path); e == nil {
149 | err = s.Update(data)
150 | }
151 |
152 | return s, err
153 | }
154 |
155 | // SetSettingsPath sets the path where to save settings
156 | func (s *Settings) SetSettingsPath(path string) {
157 | s.settingsPath = path
158 | }
159 |
160 | // Update updates the settings with the json object provided
161 | func (s *Settings) Update(data []byte) (err error) {
162 | if err = json.Unmarshal(data, s); err == nil {
163 | err = validate.Struct(s)
164 | }
165 | return
166 | }
167 |
168 | // Clone clones the settings
169 | func (s *Settings) Clone() *Settings {
170 | n := &Settings{settingsPath: s.settingsPath}
171 | if err := n.UpdateFrom(s); err != nil {
172 | panic("Failed cloning settings: " + err.Error())
173 | }
174 | return n
175 | }
176 |
177 | // UpdateFrom updates the settings with the settings object provided
178 | func (s *Settings) UpdateFrom(settings *Settings) error {
179 | return copier.Copy(s, settings)
180 | }
181 |
182 | // Save saves the current settings to path
183 | func (s *Settings) Save() (err error) {
184 | var data []byte
185 | if data, err = json.MarshalIndent(s, "", " "); err == nil {
186 | err = ioutil.WriteFile(s.settingsPath, data, 0644)
187 | }
188 | return
189 | }
190 |
--------------------------------------------------------------------------------
/util/hash.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "os"
10 | )
11 |
12 | const chunkSize = 64 * 1024
13 |
14 | func readAt(file io.ReadSeeker, buf []byte, off int64) (n int, err error) {
15 | _, err = file.Seek(off, io.SeekStart)
16 | if err == nil {
17 | return file.Read(buf)
18 | }
19 | return
20 | }
21 |
22 | // Hash file based on https://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes
23 | func HashFile(file io.ReadSeeker, size int64) (string, error) {
24 | if size < chunkSize {
25 | return "", errors.New("file is too small")
26 | }
27 |
28 | // Read head and tail blocks.
29 | buf := make([]byte, chunkSize*2)
30 | if _, err := readAt(file, buf[:chunkSize], 0); err != nil {
31 | return "", err
32 | }
33 | if _, err := readAt(file, buf[chunkSize:], size-chunkSize); err != nil {
34 | return "", err
35 | }
36 |
37 | // Convert to uint64 and sum
38 | nums := make([]uint64, (chunkSize*2)/8)
39 | reader := bytes.NewReader(buf)
40 | if err := binary.Read(reader, binary.LittleEndian, &nums); err != nil {
41 | return "", err
42 | }
43 |
44 | var hash uint64
45 | for _, num := range nums {
46 | hash += num
47 | }
48 |
49 | return fmt.Sprintf("%016x", hash+uint64(size)), nil
50 | }
51 |
52 | func Hash(path string) (string, error) {
53 | file, err := os.Open(path)
54 | if err != nil {
55 | return "", err
56 | }
57 | //noinspection GoUnhandledErrorResult
58 | defer file.Close()
59 | stats, err := file.Stat()
60 | if err != nil {
61 | return "", err
62 | }
63 | return HashFile(file, stats.Size())
64 | }
65 |
--------------------------------------------------------------------------------
/util/version.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "github.com/i96751414/libtorrent-go"
5 | )
6 |
7 | var (
8 | Version = "development"
9 | )
10 |
11 | func GetVersion() string {
12 | return Version
13 | }
14 |
15 | func UserAgent() string {
16 | return "torrest/" + GetVersion() + " libtorrent/" + libtorrent.Version()
17 | }
18 |
--------------------------------------------------------------------------------