Initial DDC release

This commit is contained in:
SirBlob committed 2026-10-25 20:24:50 -04:00
commit 434f6441da
57 files changed
+4606

No files matched your search

+73
View File
@@ -0,0 +1,73 @@
package config
import (
"errors"
"fmt"
"path/filepath"
"time"
"ddc/internal/preferences"
)
const (
minPasswordLength = 12
defaultLibraryRoots = "/media/blob/TeaNet/Plex;/media/blob/ChaiNet/Plex"
)
type Settings struct {
ListenAddr string
Username string
Password string
CookieSecure bool
SessionTTL time.Duration
TrustProxyHeaders bool
DownloadDir string
LibraryRoots []string
Defaults preferences.Preferences
}
func Load() (Settings, error) {
reader := &envReader{}
settings := Settings{
ListenAddr: reader.text("LISTEN_ADDR", ":8080"),
Username: reader.text("AUTH_USERNAME", ""),
Password: reader.text("AUTH_PASSWORD", ""),
CookieSecure: reader.boolean("COOKIE_SECURE", true),
SessionTTL: time.Duration(reader.integer("SESSION_TTL_HOURS", 12)) * time.Hour,
TrustProxyHeaders: reader.boolean("TRUST_PROXY_HEADERS", false),
DownloadDir: filepath.Clean(reader.text("DOWNLOAD_DIR", "/data/downloads")),
Defaults: preferences.Preferences{
MaxConcurrentDownloads: reader.integer("MAX_CONCURRENT_DOWNLOADS", 2),
MinFreeGB: reader.integer("MIN_FREE_GB", 10),
AllowedPorts: reader.ports("ALLOWED_PORTS", "80,443"),
AutoExtract: true,
CleanupAfterMove: true,
},
}
rawRoots := reader.text("LIBRARY_ROOTS", defaultLibraryRoots)
if reader.err != nil {
return Settings{}, reader.err
}
roots, err := parseLibraryRoots(rawRoots)
if err != nil {
return Settings{}, err
}
settings.LibraryRoots = roots
return settings, settings.validate()
}
func (settings Settings) validate() error {
switch {
case settings.Username == "":
return errors.New("AUTH_USERNAME is required")
case len(settings.Password) < minPasswordLength:
return fmt.Errorf("AUTH_PASSWORD must be at least %d characters", minPasswordLength)
case !filepath.IsAbs(settings.DownloadDir):
return errors.New("DOWNLOAD_DIR must be an absolute path")
case settings.SessionTTL <= 0:
return errors.New("SESSION_TTL_HOURS must be greater than zero")
}
return settings.Defaults.Validate()
}
+78
View File
@@ -0,0 +1,78 @@
package config
import (
"fmt"
"os"
"strconv"
"strings"
)
type envReader struct {
err error
}
func (reader *envReader) fail(name string, err error) {
if reader.err == nil {
reader.err = fmt.Errorf("%s: %w", name, err)
}
}
func (reader *envReader) text(name, fallback string) string {
value := strings.TrimSpace(os.Getenv(name))
if value == "" {
return fallback
}
return value
}
func (reader *envReader) boolean(name string, fallback bool) bool {
raw := reader.text(name, "")
if raw == "" {
return fallback
}
value, err := strconv.ParseBool(raw)
if err != nil {
reader.fail(name, err)
return fallback
}
return value
}
func (reader *envReader) integer(name string, fallback int) int {
raw := reader.text(name, "")
if raw == "" {
return fallback
}
value, err := strconv.Atoi(raw)
if err != nil {
reader.fail(name, err)
return fallback
}
return value
}
func (reader *envReader) ports(name, fallback string) []uint16 {
raw := reader.text(name, fallback)
if raw == "*" {
return nil
}
var ports []uint16
for _, part := range strings.Split(raw, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
value, err := strconv.ParseUint(part, 10, 16)
if err != nil || value == 0 {
reader.fail(name, fmt.Errorf("invalid port %q", part))
return nil
}
ports = append(ports, uint16(value))
}
if len(ports) == 0 {
reader.fail(name, fmt.Errorf("no ports listed, use * to allow any port"))
}
return ports
}
+56
View File
@@ -0,0 +1,56 @@
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
func parseLibraryRoots(raw string) ([]string, error) {
var roots []string
seenRoots := make(map[string]bool)
for _, entry := range strings.Split(raw, ";") {
entry = strings.TrimSpace(entry)
if entry == "" {
continue
}
root, err := resolveRoot(entry)
if err != nil {
return nil, err
}
if seenRoots[root] {
continue
}
seenRoots[root] = true
roots = append(roots, root)
}
if len(roots) == 0 {
return nil, errors.New("LIBRARY_ROOTS must list at least one folder")
}
return roots, nil
}
func resolveRoot(entry string) (string, error) {
cleaned := filepath.Clean(entry)
if !filepath.IsAbs(cleaned) || cleaned == "/" {
return "", fmt.Errorf("LIBRARY_ROOTS: invalid path %q", entry)
}
resolved, err := filepath.EvalSymlinks(cleaned)
if err != nil {
return "", fmt.Errorf("LIBRARY_ROOTS: %w", err)
}
info, err := os.Stat(resolved)
if err != nil {
return "", fmt.Errorf("LIBRARY_ROOTS: %w", err)
}
if !info.IsDir() {
return "", fmt.Errorf("LIBRARY_ROOTS: %q is not a directory", entry)
}
return resolved, nil
}