krz/skunky-art
Alternative privacy frontend for DeviantArt.
clone: git clone https://gitbay.org/krz/skunky-art.git
1package app
2
3import (
4 "encoding/json"
5 "os"
6 "regexp"
7 "skunkyart/static"
8 "strconv"
9 "time"
10
11 "github.com/zerolabsco/devianter"
12)
13
14// Release carries the build's version and description, set at link time and
15// shown by --help and the API.
16var Release struct {
17 Version string
18 Description string
19}
20
21type cacheConfig struct {
22 Enabled bool `json:"enabled"`
23 MemCache bool `json:"memcache"`
24 Path string `json:"path"`
25 MaxSize int64 `json:"max-size"`
26 Lifetime string `json:"lifetime"`
27 UpdateInterval int64 `json:"update-interval"`
28}
29
30type config struct {
31 cfg string
32 Listen string `json:"listen"`
33 URI string `json:"uri"`
34 Cache cacheConfig `json:"cache"`
35 Proxy bool `json:"proxy"`
36 Nsfw bool `json:"nsfw"`
37 UserAgent string `json:"user-agent"`
38 DownloadProxy string `json:"download-proxy"`
39 StaticPath string `json:"static-path"`
40}
41
42// CFG is the running instance's configuration, holding the defaults below until
43// ExecuteConfig overwrites them from the config file.
44var CFG = config{
45 cfg: "config.json",
46 Listen: "127.0.0.1:3003",
47 URI: "/",
48 Cache: cacheConfig{
49 Enabled: false,
50 Path: "cache",
51 UpdateInterval: 1,
52 },
53 StaticPath: "static",
54 UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
55 Proxy: true,
56 Nsfw: true,
57}
58
59var lifetimeParsed int64
60
61// checkCacheWritable creates the cache directory if it is missing and confirms
62// this process can actually write into it, returning the error that a real cache
63// write would hit.
64//
65// An unwritable cache directory is otherwise a silent cliff: every media request
66// still succeeds by re-downloading from the CDN, so the only symptom is one
67// "permission denied" line per request and a cache that never fills.
68func checkCacheWritable(path string) error {
69 if err := os.MkdirAll(path, 0700); err != nil {
70 return err
71 }
72 probe := path + "/.skunkyart-write-probe"
73 if err := os.WriteFile(probe, nil, 0600); err != nil {
74 return err
75 }
76 return os.Remove(probe)
77}
78
79// ExecuteConfig loads the config file into CFG, validates it, and starts the
80// cache rotation loop if caching is on. It exits the process on a config that
81// cannot be read, that asks for caching without proxying, or that points caching
82// at a directory this process cannot write.
83func ExecuteConfig() {
84 if CFG.cfg != "" {
85 f, err := os.ReadFile(CFG.cfg)
86 tryWithExitStatus(err, 1)
87 tryWithExitStatus(json.Unmarshal(f, &CFG), 1)
88 if CFG.Cache.Enabled && !CFG.Proxy {
89 exit("Incompatible settings detected: cannot use caching media content without proxy", 1)
90 }
91
92 if CFG.Cache.Enabled {
93 if err := checkCacheWritable(CFG.Cache.Path); err != nil {
94 exit("Cache directory is not writable by this process (uid "+
95 strconv.Itoa(os.Getuid())+"): "+err.Error()+
96 "\nGrant that uid write access to the directory, or set cache.enabled to false."+
97 "\nThe official container image runs as uid 10000, so a bind-mounted cache needs:"+
98 "\n chown -R 10000:10000 <cache dir on the host>", 1)
99 }
100
101 if CFG.Cache.Lifetime != "" {
102 var duration int64
103 day := 24 * time.Hour.Milliseconds()
104 numstr := regexp.MustCompile("[0-9]+").FindAllString(CFG.Cache.Lifetime, -1)
105 num, _ := strconv.Atoi(numstr[len(numstr)-1])
106
107 switch unit := CFG.Cache.Lifetime[len(CFG.Cache.Lifetime)-1:]; unit {
108 case "i":
109 duration = time.Minute.Milliseconds()
110 case "h":
111 duration = time.Hour.Milliseconds()
112 case "d":
113 duration = day
114 case "w":
115 duration = day * 7
116 case "m":
117 duration = day * 30
118 case "y":
119 duration = day * 360
120 default:
121 exit("Invalid unit specified: "+unit, 1)
122 }
123
124 lifetimeParsed = duration * int64(num)
125 }
126 // max-size is documented in megabytes. This was 1024^2, which in Go is
127 // XOR (1026), not exponentiation — so the cap was ~1000x too small.
128 CFG.Cache.MaxSize *= 1024 * 1024
129 go InitCacheSystem()
130 if CFG.Cache.MemCache {
131 go InitMemCacheJanitor()
132 }
133 }
134
135 About = instanceAbout{
136 Proxy: CFG.Proxy,
137 Nsfw: CFG.Nsfw,
138 }
139
140 static.StaticPath = CFG.StaticPath
141 devianter.UserAgent = CFG.UserAgent
142 }
143}