diff options
| -rwxr-xr-x | SETUP.md | 5 | ||||
| -rwxr-xr-x | app/config.go | 29 | ||||
| -rwxr-xr-x | compose.example.yaml | 5 | ||||
| -rw-r--r-- | compose.vpn_example.yml | 4 |
4 files changed, 39 insertions, 4 deletions
@@ -12,7 +12,10 @@ Time units: * `uri` — Instance URI. Example: `"uri":"/art/"` -> https://skunky.ebloid.ru/art/ * `cache` — Caching system; default is off. * `enabled` — Caching system state, requires boolean value - * `path` — Path to cache directory, requires absolute filesystem path + * `path` — Path to cache directory. It must be writable by the user SkunkyArt + runs as, and SkunkyArt refuses to start if it is not. The container image + runs as uid 10000, so a bind-mounted cache needs + `sudo chown -R 10000:10000 <dir>` on the host. * `lifetime` — Cached file life time, requires numeric value, followed by multiplicative suffix (see Time Units for details) * `max-size` — Maximum file size in megabytes * `update-interval` — Automatic rotation interval diff --git a/app/config.go b/app/config.go index 3b85527..ad07aa3 100755 --- a/app/config.go +++ b/app/config.go @@ -58,9 +58,28 @@ var CFG = config{ var lifetimeParsed int64 +// checkCacheWritable creates the cache directory if it is missing and confirms +// this process can actually write into it, returning the error that a real cache +// write would hit. +// +// An unwritable cache directory is otherwise a silent cliff: every media request +// still succeeds by re-downloading from the CDN, so the only symptom is one +// "permission denied" line per request and a cache that never fills. +func checkCacheWritable(path string) error { + if err := os.MkdirAll(path, 0700); err != nil { + return err + } + probe := path + "/.skunkyart-write-probe" + if err := os.WriteFile(probe, nil, 0600); err != nil { + return err + } + return os.Remove(probe) +} + // ExecuteConfig loads the config file into CFG, validates it, and starts the // cache rotation loop if caching is on. It exits the process on a config that -// cannot be read or that asks for caching without proxying. +// cannot be read, that asks for caching without proxying, or that points caching +// at a directory this process cannot write. func ExecuteConfig() { if CFG.cfg != "" { f, err := os.ReadFile(CFG.cfg) @@ -71,6 +90,14 @@ func ExecuteConfig() { } if CFG.Cache.Enabled { + if err := checkCacheWritable(CFG.Cache.Path); err != nil { + exit("Cache directory is not writable by this process (uid "+ + strconv.Itoa(os.Getuid())+"): "+err.Error()+ + "\nGrant that uid write access to the directory, or set cache.enabled to false."+ + "\nThe official container image runs as uid 10000, so a bind-mounted cache needs:"+ + "\n chown -R 10000:10000 <cache dir on the host>", 1) + } + if CFG.Cache.Lifetime != "" { var duration int64 day := 24 * time.Hour.Milliseconds() diff --git a/compose.example.yaml b/compose.example.yaml index 2e2f088..d7c20ea 100755 --- a/compose.example.yaml +++ b/compose.example.yaml @@ -14,4 +14,7 @@ services: - no-new-privileges:true volumes: - ./config.json:/config.json:ro - - ./cache:/cache # Ensure cache folder has a 10000:10000 ownership. + # The image runs as uid 10000, so the host cache dir must be writable by it: + # mkdir -p cache && sudo chown -R 10000:10000 cache + # Without this the container exits at startup telling you the same thing. + - ./cache:/cache diff --git a/compose.vpn_example.yml b/compose.vpn_example.yml index 27cd488..a89262c 100644 --- a/compose.vpn_example.yml +++ b/compose.vpn_example.yml @@ -59,7 +59,9 @@ services: - no-new-privileges:true volumes: - ./config.json:/config.json:ro - - ./cache:/cache # ensure this dir is owned 10000:10000 + # The image runs as uid 10000, so the host cache dir must be writable by it: + # mkdir -p cache && sudo chown -R 10000:10000 cache + - ./cache:/cache environment: # Empty by default = direct. Set SKUNKY_PROXY in .env to route via the VPN. - HTTPS_PROXY=${SKUNKY_PROXY:-} |
