krz/skunky-art

Alternative privacy frontend for DeviantArt.

clone: git clone https://gitbay.org/krz/skunky-art.git

v1.3.7: app/config.go · raw

  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}