├── .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 | [![Build Status](https://github.com/i96751414/torrest/workflows/build/badge.svg)](https://github.com/i96751414/torrest/actions?query=workflow%3Abuild) 4 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/14e736b811194699a98fc900979a99ad)](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 | --------------------------------------------------------------------------------