Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187package config
import ( "context" "errors" "fmt" "net/url" "path/filepath" "strings" "time"
"github.com/bluesky-social/indigo/atproto/syntax" "github.com/sethvargo/go-envconfig" "tangled.org/core/migrator/crypto")
const DefaultReturnPath = "/repo/migrate"
type Config struct { ListenAddr string `env:"MIGRATOR_LISTEN_ADDR, default=:6767"` DbPath string `env:"MIGRATOR_DB_PATH, default=migrator.db"` Hostname string `env:"MIGRATOR_HOSTNAME, required"`
PrivateKey string `env:"MIGRATOR_PRIVATE_KEY, required"` MasterKey string `env:"MIGRATOR_MASTER_KEY, required"`
// origin the browser is sent back to after an oauth grant; never taken from the request AppURL string `env:"MIGRATOR_APP_URL, default="`
PlcUrl string `env:"MIGRATOR_PLC_URL, default=https://plc.directory"`
GitHubHost string `env:"MIGRATOR_GITHUB_HOST, default=github.com"`
Concurrency int `env:"MIGRATOR_CONCURRENCY, default=4"` WorkDir string `env:"MIGRATOR_WORK_DIR, default=/tmp/migrator"`
JobTimeout time.Duration `env:"MIGRATOR_JOB_TIMEOUT, default=55m"` MaxDiskBytes int64 `env:"MIGRATOR_MAX_DISK_BYTES, default=21474836480"` // bounds the Git pack handed to knot2; LFS objects use the separate disk budget MaxPackBytes int64 `env:"MIGRATOR_MAX_PACK_BYTES, default=268435456"`
MaxActivePerOwner int `env:"MIGRATOR_MAX_ACTIVE_PER_OWNER, default=2"` APITimeout time.Duration `env:"MIGRATOR_API_TIMEOUT, default=30s"`
// go-envconfig treats env:"-" as a var name and fails, untagged fields are skipped ParsedMasterKey crypto.MasterKey ServiceDid syntax.DID}
func Load(ctx context.Context) (*Config, error) { var cfg Config if err := envconfig.Process(ctx, &cfg); err != nil { return nil, fmt.Errorf("processing environment config: %w", err) }
if err := cfg.Validate(); err != nil { return nil, err }
return &cfg, nil}
func (c *Config) ServiceURL() string { specific := strings.TrimPrefix(c.ServiceDid.String(), "did:web:") authority, path, hasPath := strings.Cut(specific, ":") origin := "https://" + unescape(authority) if !hasPath { return origin } segments := strings.Split(path, ":") for i, segment := range segments { segments[i] = unescape(segment) } return origin + "/" + strings.Join(segments, "/")}
func allDigits(s string) bool { if s == "" { return false } for _, r := range s { if r < '0' || r > '9' { return false } } return true}
func unescape(segment string) string { decoded, err := url.PathUnescape(segment) if err != nil { return segment } return decoded}
func (c *Config) Validate() error { if c.Hostname == "" { return errors.New("MIGRATOR_HOSTNAME is required") }
hostname := strings.ToLower(strings.TrimPrefix(strings.TrimSpace(c.Hostname), "did:web:")) if hostname == "" || strings.ContainsAny(hostname, "/%") { return fmt.Errorf("MIGRATOR_HOSTNAME must be a bare host, got %q", c.Hostname) } // did:web uses colons for path segments, so a port has to be percent-encoded segments := strings.Split(hostname, ":") if len(segments) > 1 && allDigits(segments[1]) { segments = append([]string{segments[0] + "%3A" + segments[1]}, segments[2:]...) } did, err := syntax.ParseDID("did:web:" + strings.Join(segments, ":")) if err != nil { return fmt.Errorf("invalid service DID from hostname %q: %w", c.Hostname, err) } c.ServiceDid = did c.Hostname = hostname
if c.PrivateKey == "" { return errors.New("MIGRATOR_PRIVATE_KEY is required") } if c.MasterKey == "" { return errors.New("MIGRATOR_MASTER_KEY is required") }
if c.MasterKey == c.PrivateKey { return errors.New("MIGRATOR_MASTER_KEY and MIGRATOR_PRIVATE_KEY must not be identical") }
c.GitHubHost = strings.ToLower(strings.TrimSpace(c.GitHubHost)) if c.GitHubHost == "" { c.GitHubHost = "github.com" }
c.AppURL = strings.TrimSuffix(strings.TrimSpace(c.AppURL), "/") if c.AppURL != "" { u, err := url.Parse(c.AppURL) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" || (u.Path != "" && u.Path != "/") || u.RawQuery != "" || u.Fragment != "" || u.User != nil { return fmt.Errorf("MIGRATOR_APP_URL must be a bare origin (scheme://host), got %q", c.AppURL) } } else { c.AppURL = strings.TrimSuffix(c.ServiceURL(), "/") }
mk, err := crypto.ParseMasterKey(c.MasterKey) if err != nil { return fmt.Errorf("invalid MIGRATOR_MASTER_KEY: %w", err) } c.ParsedMasterKey = mk
if c.Concurrency <= 0 { c.Concurrency = 4 }
absWork, err := filepath.Abs(c.WorkDir) if err != nil { return fmt.Errorf("resolving work dir %q: %w", c.WorkDir, err) } cleaned := filepath.Clean(absWork) if cleaned == "/" || cleaned == "." { return errors.New("MIGRATOR_WORK_DIR must not be root directory") } parts := strings.Split(strings.Trim(cleaned, string(filepath.Separator)), string(filepath.Separator)) if len(parts) < 2 { return fmt.Errorf("MIGRATOR_WORK_DIR %q is too shallow; must be a dedicated subdirectory", cleaned) } c.WorkDir = cleaned
if c.JobTimeout <= 0 { c.JobTimeout = 55 * time.Minute } if c.MaxDiskBytes <= 0 { c.MaxDiskBytes = 20 * 1024 * 1024 * 1024 } if c.MaxPackBytes <= 0 { c.MaxPackBytes = 256 * 1024 * 1024 } if c.MaxActivePerOwner <= 0 { c.MaxActivePerOwner = 2 } if c.APITimeout <= 0 { c.APITimeout = 30 * time.Second }
return nil}