-
Notifications
You must be signed in to change notification settings - Fork 32
/
prefetch.go
277 lines (260 loc) · 9.31 KB
/
prefetch.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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
package main
import (
"fmt"
"log"
"os"
"path"
"strings"
"time"
"github.com/gorhill/cronexpr"
)
// Gets a duration from a cron string
func getCronDuration(cronStr string, from time.Time) (time.Duration, error) {
cron, err := cronexpr.Parse(cronStr)
if err != nil {
return time.Duration(0), err // shouldn't happen cause it is being checked on creation
}
nextTick := cron.Next(from)
if nextTick.IsZero() {
return time.Duration(0), fmt.Errorf("there is no next tick")
}
return nextTick.Sub(from), nil
}
// Setups the prefetching ticker
func setupPrefetchTicker() *time.Ticker {
if config.Prefetch == nil {
log.Fatalf("Called setupPrefetchTicker with config.Prefetch uninitialized")
return nil
}
duration, err := getCronDuration(config.Prefetch.Cron, time.Now())
if err != nil {
log.Print(err)
return nil
}
if duration <= 0 {
log.Printf("Prefetching is disabled")
return nil
}
ticker := time.NewTicker(duration) // set prefetch as specified in config file
log.Printf("The prefetching routine will be run on %v", time.Now().Add(duration))
go func() {
lastTimeInvoked := time.Time{}
for range ticker.C {
if time.Since(lastTimeInvoked) > time.Second {
prefetchPackages()
lastTimeInvoked = time.Now()
now := time.Now()
duration, err := getCronDuration(config.Prefetch.Cron, time.Now())
if err == nil && duration > 0 {
ticker.Reset(duration) // update to the new timing
log.Printf("On %v the prefetching routine will be run again", now.Add(duration))
} else {
ticker.Stop()
log.Printf("Prefetching disabled")
}
} // otherwise ignore it. It happened more than once that this function gets invoked twice for no reason
}
}()
return ticker
}
// initializes the prefetchDB variable, by creating the db if it doesn't exist
func setupPrefetch() {
createPrefetchDB()
db, err := getDBConnection()
if err != nil {
log.Fatal(err)
}
prefetchDB = db
}
// function to update the db when a package is being actively requested
func updateDBRequestedFile(repoName string, fileName string) {
// don't register when signature gets downloaded, to reduce db accesses
if strings.HasSuffix(fileName, ".sig") || strings.HasSuffix(fileName, ".db") {
return
}
pkg, err := getPackageFromFilenameAndRepo(repoName, fileName)
if err != nil {
log.Printf("error: %v\n", err)
// Don't register them if they have a wrong format.
// The accepted format for a package name is name-version-subversion-arch.pkg.tar.zst
// otherwise I cannot know if a package has been updated
return
}
if prefetchDB == nil {
log.Fatal("Trying to insert data into a non-existent db")
return
}
var existentPkg Package
prefetchDB.First(&existentPkg, "packages.package_name = ? and packages.arch = ? AND packages.repo_name = ?", pkg.PackageName, pkg.Arch, pkg.RepoName)
if existentPkg.PackageName == "" {
if db := prefetchDB.Save(&pkg); db.Error != nil {
log.Printf("db error: %v\n", db.Error)
}
} else {
if existentPkg.Version == pkg.Version {
now := time.Now()
existentPkg.LastTimeDownloaded = &now
if db := prefetchDB.Save(existentPkg); db.Error != nil {
log.Printf("db error: %v\n", db.Error)
}
} else {
// if on a repo there is a different version, we assume it is the most recent one.
// The one with the bigger version number may be wrong, assuming a corner case in which a downgrade have been done in the upstream mirror.
// This is not a vulnerability, as the client specifies the version it wants
// if two mirrors serve 2 different versions of the same package, this could be a issue (it won't cache the result).
// I hope not, cause it would be nonsensical. If it has some sense, mirror name should be added as a primary key too
purgePkgIfExists(&existentPkg)
if db := prefetchDB.Save(pkg); db.Error != nil {
log.Printf("db error: %v\n", db.Error)
}
}
}
}
// function to update the db when a package gets prefetched
func updateDBPrefetchedFile(repoName string, fileName string) {
// don't register when signature gets downloaded, to reduce db accesses
if strings.HasSuffix(fileName, ".pkg.tar.zst") {
pkg, err := getPackageFromFilenameAndRepo(repoName, fileName)
if err != nil {
log.Printf("error: %v\n", err)
return
}
if prefetchDB == nil {
log.Fatal("Trying to insert data into a non-existent db")
return
}
var existentPkg Package
prefetchDB.First(&existentPkg, "packages.package_name = ? and packages.arch = ? AND packages.repo_name = ?", pkg.PackageName, pkg.Arch, pkg.RepoName)
if existentPkg.PackageName == "" {
if db := prefetchDB.Save(&pkg); db.Error != nil {
log.Printf("db error: %v\n", db.Error)
} // save it anyway
if err := fmt.Errorf("warning: prefetched package wasn't on the db"); err != nil {
log.Printf("error: %v\n", err)
return
}
} else {
if existentPkg.Version == pkg.Version {
now := time.Now()
existentPkg.LastTimeRepoUpdated = &now
if db := prefetchDB.Save(existentPkg); db.Error != nil {
log.Printf("db error: %v\n", db.Error)
}
} else {
// if on a repo there is a different version, we assume it is the most recent one.
// The one with the bigger version number may be wrong, assuming a corner case in which a downgrade have been done in the upstream mirror.
// This is not a vulnerability, as the client specifies the version it wants
// if two mirrors serve 2 different versions of the same package, this could be (a bit of an) issue cause
// pacoloco won't cache the result.
// I hope not, because it would be nonsensical. If it has some sense, mirror name should be added as a primary key too
purgePkgIfExists(&existentPkg)
if db := prefetchDB.Save(pkg); db.Error != nil {
log.Printf("db error: %v\n", db.Error)
}
}
}
}
}
// purges all possible package files
func purgePkgIfExists(pkgToDel *Package) {
if pkgToDel == nil {
return
}
basePathsToDelete := pkgToDel.getAllPaths()
for _, p := range basePathsToDelete {
pathToDelete := path.Join(config.CacheDir, p)
if _, err := os.Stat(pathToDelete); !os.IsNotExist(err) {
// if it exists, delete it
if err := os.Remove(pathToDelete); err != nil {
log.Printf("Error while trying to remove unused package %v : %v", pathToDelete, err)
}
}
}
}
// purges unused and dead packages both from db and their files and removes unused db links from the db
func cleanPrefetchDB() {
log.Printf("Cleaning the db...\n")
if config.Prefetch == nil {
log.Fatalf("Shouldn't call a prefetch purge when prefetch is not set in the yaml. This is most likely a bug.")
return
}
period := 24 * time.Hour * time.Duration(config.Prefetch.TTLUnaccessed)
olderThan := time.Now().Add(-period)
deadPkgs := getAndDropUnusedPackages(period)
dropUnusedDBFiles(olderThan) // drop too old db links
// deletes unused pkgs
for _, pkgToDel := range deadPkgs {
purgePkgIfExists(&pkgToDel)
}
period = 24 * time.Hour * time.Duration(config.Prefetch.TTLUnupdated)
olderThan = time.Now().Add(-period)
deadPkgs = getAndDropDeadPackages(olderThan)
// deletes dead packages
for _, pkgToDel := range deadPkgs {
purgePkgIfExists(&pkgToDel)
}
// delete mirror links which does not exist on the config file or are invalid
mirrors := getAllMirrorsDB()
for _, mirror := range mirrors {
if _, exists := config.Repos[mirror.RepoName]; exists {
if strings.Index(mirror.URL, "/repo/") != 0 {
log.Printf("warning: deleting %v link due to migrating to a newer version of pacoloco. Simply do 'pacman -Sy' on repo %v to fix the prefetching.", mirror.URL, mirror.RepoName)
deleteMirrorDBFromDB(mirror)
}
} else {
// there is no repo with that name, I delete the mirrorDB entry
log.Printf("Deleting %v, repo %v does not exist", mirror.URL, mirror.RepoName)
deleteMirrorDBFromDB(mirror)
}
}
// should be useless but this guarantees that everything got cleaned properly
_ = deleteMirrorPkgsTable()
log.Printf("Db cleaned.\n")
}
// This calls the actual prefetching process, should be called once the db had been cleaned
func prefetchAllPkgs() {
updateMirrorsDbs()
defer deleteMirrorPkgsTable()
pkgs, err := getPkgsToUpdate()
if err != nil {
log.Printf("Prefetching failed: %v. Are you sure you had something to prefetch?", err)
return
}
for _, p := range pkgs {
pkg := getPackage(p.PackageName, p.Arch, p.RepoName)
urls := p.getDownloadURLs()
var failed []string
for _, url := range urls {
if err := prefetchRequest(url, ""); err != nil {
failed = append(failed, fmt.Sprintf("Failed to prefetch package at %v because %v\n", url, err))
continue
}
purgePkgIfExists(&pkg) // delete the old package
if strings.HasSuffix(url, ".sig") {
log.Printf("Successfully prefetched %v-%v signature\n", p.PackageName, p.Arch)
} else {
log.Printf("Successfully prefetched %v-%v package\n", p.PackageName, p.Arch)
}
}
if len(urls)-len(failed) < 2 { // If less than 2 packages succeeded in being downloaded, show error messages
for _, msg := range failed {
log.Println(msg)
}
}
}
}
// the prefetching routine
func prefetchPackages() {
if prefetchDB == nil {
return
}
log.Printf("Starting prefetching routine...\n")
// update mirrorlists from file if they exist
// purge all useless files
cleanPrefetchDB()
// prefetch all Packages
log.Printf("Starting prefetching packages...\n")
prefetchAllPkgs()
log.Printf("Finished prefetching packages!\n")
log.Printf("Finished prefetching routine!\n")
}