forked from anatol/pacoloco
-
Notifications
You must be signed in to change notification settings - Fork 0
/
prefetch.go
266 lines (251 loc) · 9.41 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
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() {
duration := nextTick.Sub(from)
return duration, err
}
return time.Duration(0), fmt.Errorf("there is no next tick")
}
// Setups the prefetching ticker
func setupPrefetchTicker() *time.Ticker {
if config.Prefetch != nil {
duration, err := getCronDuration(config.Prefetch.Cron, time.Now())
if err == nil && duration > 0 {
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
}
log.Printf("Prefetching is disabled")
return nil
}
log.Fatalf("Called setupPrefetchTicker with config.Prefetch uninitialized")
return nil
}
// 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") {
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 {
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)
}
}
}
} else {
log.Fatal("Trying to insert data into a non-existent db")
}
}
}
// 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 {
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)
}
}
}
} else {
log.Fatal("Trying to insert data into a non-existent db")
}
}
}
// purges all possible package files
func purgePkgIfExists(pkgToDel *Package) {
if pkgToDel != nil {
basePathsToDelete := getAllPackagePaths(*pkgToDel)
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 {
period := time.Duration(24 * int64(time.Hour) * int64(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 = time.Duration(24 * int64(time.Hour) * int64(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")
} else {
log.Fatalf("Shouldn't call a prefetch purge when prefetch is not set in the yaml. This is most likely a bug.")
}
}
// This calls the actual prefetching process, should be called once the db had been cleaned
func prefetchAllPkgs() {
updateMirrorsDbs()
defer deleteMirrorPkgsTable()
pkgs := getPkgsToUpdate()
for _, p := range pkgs {
pkg := getPackage(p.PackageName, p.Arch, p.RepoName)
urls := getPkgToUpdateDownloadURLs(p)
var failed []string
for _, url := range urls {
if err := prefetchRequest(url, ""); err == nil {
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)
}
} else {
failed = append(failed, fmt.Sprintf("Failed to prefetch package at %v because %v\n", url, err))
}
}
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 {
log.Printf("Starting prefetching routine...\n")
// update mirrorlists from file if they exist
updateMirrorlists()
// 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")
}
}