feat(client): lodge remote binds a repository to another server
This commit is contained in:
1 parent
45c81cfcaf
commit
91d83f3032
10 files changed
+639
-147
No files matched your search
@@ -10,6 +10,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -29,14 +30,23 @@ const (
|
||||
|
||||
var (
|
||||
httpClient = &http.Client{Transport: newTransport()}
|
||||
ErrNotLoggedIn = errors.New("not logged in: run the install command from the lodge admin page")
|
||||
ErrNotLoggedIn = errors.New("not logged in: run the install command from the lodge admin page, or lodge remote <url> <code> inside the repository")
|
||||
errConflict = errors.New("server state changed")
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Servers map[string]string `json:"servers,omitempty"`
|
||||
Repos map[string]string `json:"repos,omitempty"`
|
||||
DataDir string `json:"-"`
|
||||
path string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
URL string `json:"url"`
|
||||
Token string `json:"token"`
|
||||
DataDir string `json:"-"`
|
||||
URL string
|
||||
Token string
|
||||
DataDir string
|
||||
}
|
||||
|
||||
func configPath() string {
|
||||
@@ -49,30 +59,128 @@ func dataDir() string {
|
||||
return filepath.Join(cmp.Or(os.Getenv("XDG_DATA_HOME"), filepath.Join(home, ".local", "share")), "lodge")
|
||||
}
|
||||
|
||||
func Load() (*Client, error) {
|
||||
c := &Client{DataDir: dataDir()}
|
||||
if data, err := os.ReadFile(configPath()); err == nil {
|
||||
if err := json.Unmarshal(data, c); err != nil {
|
||||
func serverURL(raw string) string {
|
||||
return strings.TrimSuffix(raw, "/")
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
return loadConfig(configPath(), dataDir())
|
||||
}
|
||||
|
||||
func loadConfig(file, dir string) (*Config, error) {
|
||||
cfg := &Config{DataDir: dir, path: file}
|
||||
if data, err := os.ReadFile(file); err == nil {
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
c.URL = strings.TrimSuffix(cmp.Or(os.Getenv("LODGE_URL"), c.URL), "/")
|
||||
c.Token = cmp.Or(os.Getenv("LODGE_TOKEN"), c.Token)
|
||||
if c.URL == "" || c.Token == "" {
|
||||
return nil, ErrNotLoggedIn
|
||||
}
|
||||
return c, nil
|
||||
cfg.URL = serverURL(cmp.Or(os.Getenv("LODGE_URL"), cfg.URL))
|
||||
cfg.Token = cmp.Or(os.Getenv("LODGE_TOKEN"), cfg.Token)
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func Login(ctx context.Context, serverURL, code string) error {
|
||||
name, _ := os.Hostname()
|
||||
c := &Client{URL: strings.TrimSuffix(serverURL, "/")}
|
||||
var claim protocol.ClaimResponse
|
||||
if err := c.call(ctx, http.MethodPost, "/api/claim", protocol.ClaimRequest{Code: code, Name: name}, &claim); err != nil {
|
||||
func (cfg *Config) reload() (*Config, error) {
|
||||
return loadConfig(cfg.path, cfg.DataDir)
|
||||
}
|
||||
|
||||
func (cfg *Config) update(change func(*Config)) error {
|
||||
unlock, err := flock(cfg.path + ".lock")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Token = claim.Token
|
||||
return writeJSONFile(configPath(), c)
|
||||
defer unlock()
|
||||
saved := &Config{Servers: map[string]string{}, Repos: map[string]string{}}
|
||||
if data, err := os.ReadFile(cfg.path); err == nil {
|
||||
if err := json.Unmarshal(data, saved); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
change(saved)
|
||||
if err := writeJSONFile(cfg.path, saved); err != nil {
|
||||
return err
|
||||
}
|
||||
fresh, err := cfg.reload()
|
||||
if err == nil {
|
||||
*cfg = *fresh
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (cfg *Config) serverOf(key string) string {
|
||||
return cmp.Or(cfg.Repos[key], cfg.URL)
|
||||
}
|
||||
|
||||
func (cfg *Config) Bound(key string) bool {
|
||||
return cfg.Repos[key] != ""
|
||||
}
|
||||
|
||||
func (cfg *Config) Server(key string) (*Client, error) {
|
||||
return cfg.client(cfg.serverOf(key))
|
||||
}
|
||||
|
||||
func (cfg *Config) client(server string) (*Client, error) {
|
||||
token := cfg.Servers[server]
|
||||
if server == cfg.URL {
|
||||
token = cmp.Or(cfg.Token, token)
|
||||
}
|
||||
switch {
|
||||
case server == "":
|
||||
return nil, ErrNotLoggedIn
|
||||
case token == "":
|
||||
return nil, fmt.Errorf("no token for %s: run lodge remote %s <code> with a code from that server", server, server)
|
||||
}
|
||||
return &Client{URL: server, Token: token, DataDir: cfg.DataDir}, nil
|
||||
}
|
||||
|
||||
func (cfg *Config) clients() map[Client]bool {
|
||||
clients := map[Client]bool{}
|
||||
for _, server := range append(slices.Collect(maps.Keys(cfg.Servers)), cfg.URL) {
|
||||
if c, err := cfg.client(server); err == nil {
|
||||
clients[*c] = true
|
||||
}
|
||||
}
|
||||
return clients
|
||||
}
|
||||
|
||||
func claim(ctx context.Context, server, code string) (string, error) {
|
||||
name, _ := os.Hostname()
|
||||
var res protocol.ClaimResponse
|
||||
err := (&Client{URL: server}).call(ctx, http.MethodPost, "/api/claim", protocol.ClaimRequest{Code: code, Name: name}, &res)
|
||||
return res.Token, err
|
||||
}
|
||||
|
||||
func (cfg *Config) Login(ctx context.Context, server, code string) error {
|
||||
server = serverURL(server)
|
||||
token, err := claim(ctx, server, code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cfg.update(func(saved *Config) { saved.URL, saved.Token = server, token })
|
||||
}
|
||||
|
||||
func (cfg *Config) Connect(ctx context.Context, server, code string) error {
|
||||
server = serverURL(server)
|
||||
token, err := claim(ctx, server, code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cfg.update(func(saved *Config) { saved.Servers[server] = token })
|
||||
}
|
||||
|
||||
func (cfg *Config) Bind(key, server string) error {
|
||||
server = serverURL(server)
|
||||
if server != "" {
|
||||
if _, err := cfg.client(server); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return cfg.update(func(saved *Config) {
|
||||
if server == "" {
|
||||
delete(saved.Repos, key)
|
||||
} else {
|
||||
saved.Repos[key] = server
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func writeJSONFile(path string, value any) error {
|
||||
@@ -205,20 +313,20 @@ func (c *Client) uploadBlob(ctx context.Context, hash string) error {
|
||||
return res.Body.Close()
|
||||
}
|
||||
|
||||
func (c *Client) registryPath() string {
|
||||
return filepath.Join(c.DataDir, "worktrees.json")
|
||||
func (cfg *Config) registryPath() string {
|
||||
return filepath.Join(cfg.DataDir, "worktrees.json")
|
||||
}
|
||||
|
||||
func (c *Client) Worktrees() []string {
|
||||
func (cfg *Config) Worktrees() []string {
|
||||
var roots []string
|
||||
if data, err := os.ReadFile(c.registryPath()); err == nil {
|
||||
if data, err := os.ReadFile(cfg.registryPath()); err == nil {
|
||||
_ = json.Unmarshal(data, &roots)
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
func (c *Client) register(root string) error {
|
||||
roots := c.Worktrees()
|
||||
func (cfg *Config) register(root string) error {
|
||||
roots := cfg.Worktrees()
|
||||
if slices.Contains(roots, root) {
|
||||
return nil
|
||||
}
|
||||
@@ -226,10 +334,10 @@ func (c *Client) register(root string) error {
|
||||
_, err := os.Stat(known)
|
||||
return err != nil
|
||||
})
|
||||
return writeJSONFile(c.registryPath(), append(roots, root))
|
||||
return writeJSONFile(cfg.registryPath(), append(roots, root))
|
||||
}
|
||||
|
||||
func (c *Client) Forget(root string) error {
|
||||
roots := slices.DeleteFunc(c.Worktrees(), func(known string) bool { return known == root })
|
||||
return writeJSONFile(c.registryPath(), roots)
|
||||
func (cfg *Config) Forget(root string) error {
|
||||
roots := slices.DeleteFunc(cfg.Worktrees(), func(known string) bool { return known == root })
|
||||
return writeJSONFile(cfg.registryPath(), roots)
|
||||
}
|
||||
Reference in new issue
Block a user