CI / build (push) Successful in 18s
The client's Settings > T5DE chooses the hub from the next start on (POST /hub/server, kept in server.json); without a choice, -channel (which the client passes) decides: staging for nightly and dev, T5DE's hub for releases. $T5DE_HUB still wins. Each hub's sign-in, product IDs and products are kept in their own folder (T5DE's hub in the data folder as before, others in hubs/<host>), so a hub never gets another's token and product IDs from two hubs never clash; a sign-in saved for another hub is ignored. Signing in always goes to this run's hub. Co-Authored-By: Claude Opus 5.5 <[email protected]>
115 lines
2.9 KiB
Go
115 lines
2.9 KiB
Go
package control
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// T5DE's hubs.
|
|
const (
|
|
Production = "https://hub.t5de.dev"
|
|
Staging = "https://hub.staging.t5de.dev"
|
|
)
|
|
|
|
// ChannelDefault is the hub an install uses unless the person chooses one: nightlies and builds outside an
|
|
// install (channel "nightly", "dev") use staging, releases T5DE's hub.
|
|
func ChannelDefault(channel string) string {
|
|
if channel == "nightly" || channel == "dev" {
|
|
return Staging
|
|
}
|
|
return Production
|
|
}
|
|
|
|
// Server is the hub for this run: env ($T5DE_HUB) if set, else the person's choice (the client's Settings, kept
|
|
// in choicePath), else the channel's.
|
|
func Server(choicePath, channel, env string) string {
|
|
if env != "" {
|
|
return strings.TrimSuffix(env, "/")
|
|
}
|
|
if c := LoadChoice(choicePath); c != "" {
|
|
return c
|
|
}
|
|
return ChannelDefault(channel)
|
|
}
|
|
|
|
// LoadChoice is the hub the person chose, "" for the channel's.
|
|
func LoadChoice(path string) string {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var c struct {
|
|
Hub string `json:"hub"`
|
|
}
|
|
if json.Unmarshal(b, &c) != nil {
|
|
return ""
|
|
}
|
|
hub, err := CheckHub(c.Hub)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return hub
|
|
}
|
|
|
|
// SaveChoice remembers the hub the person chose ("" for the channel's).
|
|
func SaveChoice(path, hub string) error {
|
|
if hub == "" {
|
|
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
b, _ := json.Marshal(map[string]string{"hub": hub})
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
return err
|
|
}
|
|
tmp := path + ".tmp"
|
|
if err := os.WriteFile(tmp, b, 0o600); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, path)
|
|
}
|
|
|
|
// CheckHub is a hub's address without a trailing slash, if it's https (or this computer's, for development).
|
|
func CheckHub(s string) (string, error) {
|
|
s = strings.TrimSuffix(strings.TrimSpace(s), "/")
|
|
u, err := url.Parse(s)
|
|
if err != nil || u.Host == "" || u.RawQuery != "" || u.Fragment != "" {
|
|
return "", fmt.Errorf("not a hub address: %q", s)
|
|
}
|
|
local := u.Hostname() == "localhost" || u.Hostname() == "127.0.0.1"
|
|
if u.Scheme != "https" && !(u.Scheme == "http" && local) {
|
|
return "", errors.New("the hub needs an https address")
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// StateDir is where the proxy keeps what belongs to a hub (its sign-in, product IDs, products): the data folder
|
|
// itself for T5DE's hub, hubs/<name> in it for any other, so a hub never gets another's sign-in and product IDs
|
|
// from two hubs never clash.
|
|
func StateDir(data, hub string) string {
|
|
if hub == Production {
|
|
return data
|
|
}
|
|
u, err := url.Parse(hub)
|
|
name := hub
|
|
if err == nil {
|
|
name = u.Host + u.Path
|
|
}
|
|
name = strings.Map(func(r rune) rune {
|
|
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '.' || r == '-' {
|
|
return r
|
|
}
|
|
if r >= 'A' && r <= 'Z' {
|
|
return r + 'a' - 'A'
|
|
}
|
|
return '_'
|
|
}, name)
|
|
return filepath.Join(data, "hubs", name)
|
|
}
|