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