summaryrefslogtreecommitdiff
path: root/app/cache.go
blob: 91c9847588d08d3e18c3df163cfe08f2d846f46e (plain) (blame)
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
package app

// TODO: implement JSON caching and clean up the code.

import (
	"crypto/sha1" //nolint:gosec // G505: SHA-1 is a cache-key hash here, not a security primitive
	"encoding/hex"
	"io"
	"os"
	"strings"
	"sync"
	"syscall"
	"time"
)

type file struct {
	Score   int
	Content []byte
}

var tempFS = make(map[[20]byte]*file)
var mx = &sync.RWMutex{}

// DownloadAndSendMedia proxies one image from DeviantArt's wixmp CDN to the
// client, serving it from the on-disk or in-memory cache when enabled. It
// responds 403 when proxying is turned off for this instance.
func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
	var url strings.Builder
	url.WriteString("https://images-wixmp-")
	url.WriteString(subdomain)
	url.WriteString(".wixmp.com/")
	url.WriteString(path)
	if t := s.Args.Get("token"); t != "" {
		url.WriteString("?token=")
		url.WriteString(t)
	}

	var response []byte

	switch {
	case CFG.Cache.Enabled:
		fileName := sha1.Sum([]byte(subdomain + path)) //nolint:gosec // G401: cache-key hash, not a security primitive
		filePath := CFG.Cache.Path + "/" + hex.EncodeToString(fileName[:])

		c := func() {
			// filePath is built from a SHA-1 of the request, not from user input,
			// so it cannot escape the cache directory.
			file, err := os.Open(filePath) //nolint:gosec // G304: path is a hash, not user-controlled
			if err != nil {
				dwnld := Download(url.String())
				if dwnld.Status == 200 && strings.HasPrefix(dwnld.Headers.Get("Content-Type"), "image") {
					response = dwnld.Body
					try(os.WriteFile(filePath, response, 0600))
				} else {
					s.ReturnHTTPError(dwnld.Status)
					return
				}
			} else {
				defer func() { try(file.Close()) }()
				file, e := io.ReadAll(file)
				try(e)
				response = file
			}
		}

		if CFG.Cache.MemCache {
			mx.Lock()
			if tempFS[fileName] == nil {
				tempFS[fileName] = &file{}
			}
			mx.Unlock()

			if tempFS[fileName].Content != nil {
				response = tempFS[fileName].Content
				tempFS[fileName].Score += 2
				break
			} else {
				c()
				go func() {
					defer restore()

					mx.RLock()
					tempFS[fileName].Content = response
					mx.RUnlock()

					for {
						time.Sleep(1 * time.Minute)

						mx.Lock()
						if tempFS[fileName].Score <= 0 {
							delete(tempFS, fileName)
							mx.Unlock()
							return
						}
						tempFS[fileName].Score--
						mx.Unlock()
					}
				}()
			}
		} else {
			c()
		}
	case CFG.Proxy:
		dwnld := Download(url.String())
		if dwnld.Status != 200 {
			s.ReturnHTTPError(dwnld.Status)
			return
		}
		response = dwnld.Body
	default:
		s.Writer.WriteHeader(403)
		response = []byte("Sorry, butt proxy on this instance are disabled.")
	}

	_, _ = s.Writer.Write(response)
}

// InitCacheSystem runs the cache rotation loop forever, evicting files past
// their lifetime and emptying the cache when it outgrows max-size. Run it in its
// own goroutine.
func InitCacheSystem() {
	c := &CFG.Cache
	for {
		dir, err := os.ReadDir(c.Path)
		if err != nil {
			if os.IsNotExist(err) {
				try(os.Mkdir(c.Path, 0700))
				continue
			}
			println(err.Error())
		}

		var total int64
		for _, file := range dir {
			fileName := c.Path + "/" + file.Name()
			fileInfo, err := file.Info()
			try(err)

			if c.Lifetime != "" {
				now := time.Now().UnixMilli()

				// Sys() is platform-specific and only documented to be a
				// *syscall.Stat_t on unix; skip rotation rather than panic
				// if the filesystem reports something else.
				if stat, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
					if statTime(stat)+lifetimeParsed <= now {
						try(os.RemoveAll(fileName))
					}
				}
			}

			total += fileInfo.Size()
			// if c.MaxSize != 0 && fileInfo.Size() > c.MaxSize {
			// 	try(os.RemoveAll(fileName))
			// }
		}

		if c.MaxSize != 0 && total > c.MaxSize {
			try(os.RemoveAll(c.Path))
			try(os.Mkdir(c.Path, 0700))
		}

		time.Sleep(time.Second * time.Duration(c.UpdateInterval))
	}
}