/
client.go
217 lines (196 loc) · 5.99 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
package torrent
import (
"bytes"
"encoding/json"
"fmt"
"github.com/anacrolix/torrent"
log "github.com/sirupsen/logrus"
"net/http"
"net/url"
"time"
)
var trackers = [...]string{
"udp://open.demonii.com:1337/announce",
"udp://tracker.openbittorrent.com:80",
"udp://tracker.coppersurfer.tk:6969",
"udp://glotorrents.pw:6969/announce",
"udp://tracker.opentrackr.org:1337/announce",
"udp://torrent.gresille.org:80/announce",
"udp://p4p.arenabg.com:1337",
"udp://tracker.leechers-paradise.org:6969",
}
//Client stores active Torrents and a reference to the BitTorrent client.
type Client struct {
Client *torrent.Client
Torrents map[string]*Torrent
}
//Torrent stores general information about a torrent with flag indicating information and torrent availability.
type Torrent struct {
*torrent.Torrent
Fetched bool
}
//Status stores Information about the download progress of a torrent.
type Status struct {
Name string
InfoHash string
BytesDownloaded int64
BytesMissing int64
}
//NewClient creates a new BitTorrent client.
func NewClient() (client *Client, err error) {
var c *torrent.Client
client = &Client{}
client.Torrents = make(map[string]*Torrent)
//config
torrentCfg := torrent.NewDefaultClientConfig()
torrentCfg.Seed = false
torrentCfg.DataDir = "./Movies"
// Create client.
c, err = torrent.NewClient(torrentCfg)
if err != nil {
return client, fmt.Errorf("creating torrent client failed: %v", err)
}
client.Client = c
return
}
//AddTorrent adds Torrent to the client. If the torrent is already added returns without error.
func (c *Client) AddTorrent(infoHash string) (err error) {
//if torrent already registered in client return
if _, ok := c.Torrents[infoHash]; ok {
return nil
}
t, err := c.Client.AddMagnet(BuildMagnet(infoHash, infoHash))
if err != nil {
return fmt.Errorf("adding torrent failed: %v", err)
}
c.Torrents[infoHash] = &Torrent{Torrent: t}
//wait for fetch to Download torrent
go func() {
<-t.GotInfo()
t.DownloadAll()
c.Torrents[infoHash].Fetched = true
}()
return
}
func (c *Client) getLargestFile(infoHash string) (*torrent.File, error) {
var target *torrent.File
var maxSize int64
t, ok := c.Torrents[infoHash]
if !ok {
return nil, fmt.Errorf("error: unregistered infoHash")
}
for _, file := range t.Files() {
if maxSize < file.Length() {
maxSize = file.Length()
target = file
}
}
return target, nil
}
//MovieRequest adds a torrent identified by an info hash to the BitTorrent Client.
func (c *Client) MovieRequest(w http.ResponseWriter, r *http.Request) {
infoHash, err := infoHashFromRequest(r)
if err != nil {
log.WithField("infoHash", infoHash).Warn("MovieRequest: Request without InfoHash")
w.WriteHeader(http.StatusBadRequest)
return
}
log.WithField("infoHash", infoHash).Debug("torrent request received")
if err = c.AddTorrent(infoHash); err != nil {
log.WithField("infoHash", infoHash).Error("MovieRequest adding torrent: %v", err)
w.WriteHeader(http.StatusBadRequest)
return
}
//allow polling for state of torrent
//torrent fetched --> HTTP 202 Accepted
//torrent not yet fetched --> HTTP 200 OK => Client should continue polling state.
if c.Torrents[infoHash].Fetched {
w.WriteHeader(http.StatusAccepted)
return
}
w.WriteHeader(http.StatusOK)
}
//MovieDelete deletes a torrent identified by an info hash from the BitTorrent Client.
func (c *Client) MovieDelete(w http.ResponseWriter, r *http.Request) {
infoHash, err := infoHashFromRequest(r)
if err != nil {
log.WithField("infoHash", infoHash).Warn("MovieDelete: Request without InfoHash")
w.WriteHeader(http.StatusBadRequest)
return
}
log.WithField("infoHash", infoHash).Debug("movie delete request received")
if c.Torrents[infoHash] == nil {
log.Warn("MovieDelete: tried to delete non-existent: %s", r.URL.String())
w.WriteHeader(http.StatusNotFound)
return
}
c.Torrents[infoHash].Torrent.Drop()
delete(c.Torrents, infoHash)
//TODO: delete movie from fs
}
//TorrentStatus returns download progress information about all active torrents.
func (c *Client) TorrentStatus(w http.ResponseWriter, r *http.Request) {
stats := make([]Status, 0, 8)
for _, t := range c.Torrents {
if !t.Fetched {
continue
}
stats = append(stats, Status{
Name: t.Name(),
InfoHash: t.InfoHash().String(),
BytesDownloaded: t.BytesCompleted(),
BytesMissing: t.BytesMissing(),
})
}
err := json.NewEncoder(w).Encode(stats)
if err != nil {
log.Errorf("TorrentStatus encoding torrent status failed")
}
}
// GetFile is an http handler to serve the biggest file managed by the client.
func (c *Client) GetFile(w http.ResponseWriter, r *http.Request) {
infoHash, err := infoHashFromRequest(r)
if err != nil {
log.WithField("infoHash", infoHash).Warn("GetFile: Request without InfoHash")
w.WriteHeader(http.StatusBadRequest)
return
}
log.WithField("infoHash", infoHash).Debug("movie file request received")
target, err := c.getLargestFile(infoHash)
if err != nil {
log.WithField("infoHash", infoHash).WithError(err).Errorf("server: error getting file")
w.WriteHeader(http.StatusNotFound)
return
}
entry, err := NewFileReader(target)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer func() {
if err := entry.Close(); err != nil {
log.Printf("Error closing file reader: %s\n", err)
}
}()
http.ServeContent(w, r, target.DisplayPath(), time.Now(), entry)
}
func infoHashFromRequest(r *http.Request) (string, error) {
packed, ok := r.URL.Query()["infoHash"]
if !ok || len(packed) < 1 {
return "", fmt.Errorf("infoHashFromRequest: no infoHash in Request")
}
return packed[0], nil
}
//BuildMagnet builds a magnet link from an info hash and a static list of trackers.
func BuildMagnet(infoHash string, title string) string {
b := &bytes.Buffer{}
b.WriteString("magnet:?xt=urn:btih:")
b.WriteString(infoHash)
b.WriteString("&dn=")
b.WriteString(url.QueryEscape(title))
for _, tracker := range trackers {
b.WriteString("&tr=")
b.WriteString(tracker)
}
return b.String()
}