krz/skunky-art

Alternative privacy frontend for DeviantArt.

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

sonarcloud-cleanup: 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/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}