This repository has been archived by the owner on Nov 21, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
/
torrent.go
244 lines (207 loc) · 6.04 KB
/
torrent.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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
package main
import (
"io"
"io/fs"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/COSI-Lab/logging"
"github.com/gocolly/colly"
)
// HandleTorrents periodically downloads remote torrents and extracts torrents from disk
func HandleTorrents(config *ConfigFile, torrentDir, downloadDir string) {
err := os.MkdirAll(downloadDir, 0755)
if err != nil {
logging.Error("Failed to create torrents downloadDir: ", err)
return
}
err = os.MkdirAll(torrentDir, 0755)
if err != nil {
logging.Error("Failed to create torrents torrentDir: ", err)
return
}
// On startup, and then every day at midnight
// - scrape torrents from upstreams (such as linuxmint)
// - search disk for torrent files and corresponding downloads
// - sync downloadDir
// - sync torrentDir
go scrapeTorrents(config.Torrents, torrentDir)
go syncTorrents(config, torrentDir, downloadDir)
// Sleep until midnight
now := time.Now()
midnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.Local)
time.Sleep(time.Until(midnight))
go scrapeTorrents(config.Torrents, torrentDir)
go syncTorrents(config, torrentDir, downloadDir)
ticker := time.NewTicker(24 * time.Hour)
for range ticker.C {
go scrapeTorrents(config.Torrents, torrentDir)
go syncTorrents(config, torrentDir, downloadDir)
}
}
// syncTorrents goes over all projects, finds their torrent files, the corresponding source
// files and then creates hardlinks in the download and torrent directories
func syncTorrents(config *ConfigFile, torrentDir, ourDir string) {
for _, project := range config.GetProjects() {
if project.Torrents == "" {
continue
}
go func(project Project) {
// Find all torrent files using glob
matches, err := filepath.Glob(project.Torrents + "*.torrent")
if err != nil {
logging.Error("Failed to find torrent files: ", err)
return
}
for _, torrentPath := range matches {
fileName := strings.TrimSuffix(path.Base(torrentPath), ".torrent")
addFile(project, ourDir, fileName)
// Add the torrent file _after_ copying the actual file
// This should skip the verification step
torrentName := path.Base(torrentPath)
_, err = os.Stat(torrentDir + "/" + torrentName)
if err != nil {
if os.IsNotExist(err) {
// Create a hardlink
err = os.Link(torrentPath, torrentDir+"/"+torrentName)
if err != nil {
logging.Warn("Failed to create hardlink: ", err)
continue
}
} else {
logging.Error("Failed to stat a torrent file: ", err)
}
}
}
}(project)
}
}
// Fetches a file from a glob and a name. Saves it to downloadDir
func addFile(project Project, downloadDir, fileName string) {
// Search the glob for the corresponding file
files, err := filepath.Glob(project.Torrents + fileName)
if err != nil {
return
}
if len(files) == 0 {
return
}
// In case there are multiple files, pick the first one that correctly resolves to a file
var file string
for _, f := range files {
stat, err := os.Stat(f)
if err != nil {
logging.Warn("Failed to stat file: ", err)
continue
}
// Skip symlinks and directories
if stat.Mode()&os.ModeSymlink != 0 || stat.IsDir() {
continue
}
file = f
break
}
// Get ownership information of the download directory
info, err := os.Stat(downloadDir)
if err != nil {
logging.Warn("Failed to stat downloadDir: ", err)
return
}
// gid is that of the downloadDir
stat := info.Sys().(*syscall.Stat_t)
gid := int(stat.Gid)
// uid is the current user
uid := os.Geteuid()
// Check if the file is already in the download directory
_, err = os.Stat(downloadDir + "/" + fileName)
if err != nil {
if os.IsNotExist(err) {
// Create a hardlink
err = os.Link(file, downloadDir+"/"+fileName)
if err != nil {
logging.Warn("Failed to create hardlink: ", err)
}
err = os.Chown(downloadDir+"/"+fileName, uid, gid)
if err != nil {
logging.Warn("Failed to chown file: ", err)
}
// Make the file group writable
err = os.Chmod(downloadDir+"/"+fileName, fs.FileMode(0775))
if err != nil {
logging.Warn("Failed to chmod file: ", err)
}
} else {
logging.Error("Failed to stat a torrent file: ", err)
}
}
}
// scrapeTorrents downloads all torrents from upstreams
func scrapeTorrents(torrents []*Torrent, downloadDir string) {
for _, upstream := range torrents {
go scrape(upstream.Depth, upstream.Delay, upstream.Url, downloadDir)
}
}
// Visits a url and downloads all torrents to outdir
//
// Torrents with a name that already exists are skipped if
// the upstream file has the same file size as the one on disk
func scrape(depth, delay int, url, outdir string) {
logging.Info("Scraping " + url)
// Instantiate default collector
c := colly.NewCollector(
// MaxDepth is 1, so only the links on the scraped page
// is visited, and no further links are followed
colly.MaxDepth(depth + 1),
)
c.Limit(&colly.LimitRule{
DomainGlob: "*",
Delay: time.Duration(delay) * time.Second,
})
// On every a element which has href attribute call callback
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Attr("href")
// Visit link found on page
e.Request.Visit(link)
})
// Before making a request print "Visiting ..."
c.OnRequest(func(r *colly.Request) {
pos := strings.LastIndex(r.URL.Path, ".")
if pos != -1 && r.URL.Path[pos+1:len(r.URL.Path)] == "torrent" {
// Check if we already have this file by name
name := path.Base(r.URL.Path)
file := outdir + "/" + name
_, err := os.Stat(file)
if err != nil {
if os.IsNotExist(err) {
// Download
download(r, file)
} else {
// Unrecoverable error
logging.Warn(err)
}
}
}
})
c.Visit(url)
logging.Success("Finished scraping " + url)
}
// Downloads the file at `r` and saves it to `target` on disk
func download(r *colly.Request, target string) error {
res, err := http.Get(r.URL.String())
if err != nil {
return err
}
f, err := os.Create(target)
if err != nil {
return err
}
_, err = io.Copy(f, res.Body)
if err != nil {
return err
}
return res.Body.Close()
}