-
Notifications
You must be signed in to change notification settings - Fork 1
/
workshopimg.go
136 lines (109 loc) · 2.63 KB
/
workshopimg.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
package workshopimg
import (
"errors"
"io"
"net/http"
"strconv"
"github.com/dgraph-io/badger"
"github.com/diamondburned/distant-front/internal/httperr"
"github.com/diamondburned/distant-front/lib/workshop"
"github.com/go-chi/chi"
)
// CacheOpts is the options for the workshop cache.
type CacheOpts struct {
// CachePath is the path to the cache database.
CachePath string
}
// NoCache disables caching.
var NoCache = CacheOpts{}
// Mount mounts the workshop image route.
func Mount(cacheOpts CacheOpts) (http.Handler, error) {
var c cache
r := chi.NewRouter()
r.Get("/{workshopID}", c.redirectImage)
if cacheOpts.CachePath != "" {
if err := c.open(cacheOpts.CachePath); err != nil {
return r, err
}
}
return r, nil
}
// ErrNotFound is used when a workshop file is not found.
var ErrNotFound = httperr.New(404, "file not found")
func verifyID(id string) bool {
// max u32
_, err := strconv.ParseUint(id, 10, 32)
return err == nil
}
type cache struct {
db *badger.DB
}
func (c *cache) open(path string) (err error) {
opts := badger.LSMOnlyOptions(path)
opts.EventLogging = false
opts.SyncWrites = false
opts.Truncate = true
c.db, err = badger.Open(opts)
return
}
func (c *cache) redirectImage(w http.ResponseWriter, r *http.Request) {
url, err := c.getFile(chi.URLParam(r, "workshopID"))
if err != nil {
httperr.WriteErr(w, err)
io.WriteString(w, err.Error())
return
}
size, err := strconv.Atoi(r.FormValue("size"))
if err == nil {
url = workshop.SizedImageURL(url, size)
}
// 301 for caching.
http.Redirect(w, r, url, http.StatusMovedPermanently)
}
func (c *cache) getFile(workshopID string) (string, error) {
if !verifyID(workshopID) {
return "", httperr.New(400, "invalid ID")
}
if c.db == nil {
return c.getFileDirect(workshopID)
}
var imageURL string
var found bool
c.db.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(workshopID))
if err != nil {
return err
}
return item.Value(func(url []byte) error {
imageURL = string(url)
found = true
return nil
})
})
if found {
if imageURL != "" {
return imageURL, nil
}
return "", ErrNotFound
}
v, err := c.getFileDirect(workshopID)
if err != nil {
return "", err
}
// best effort update - skip error
c.db.Update(func(txn *badger.Txn) error {
txn.Set([]byte(workshopID), []byte(v))
return nil
})
return v, nil
}
func (c *cache) getFileDirect(workshopID string) (string, error) {
f, err := workshop.GetFile(workshopID)
if err == nil {
return f.Image, nil
}
if errors.Is(err, workshop.ErrFileNotFound) {
return "", ErrNotFound
}
return "", httperr.Wrap(err, 500, "Steam Workshop error")
}