Initial DDC release
This commit is contained in:
commit
434f6441da
57 files changed
+4606
No files matched your search
Executable
+73
@@ -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()
|
||||
}
|
||||
Executable
+78
@@ -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
|
||||
}
|
||||
Executable
+56
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user