95 lines
3.1 KiB
Go
95 lines
3.1 KiB
Go
// Copyright (C) 2026 Dcntrlized
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published
|
|
// by the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
// Package instance holds the instance's public identity: a small, self-
|
|
// contained file at the data root that answers "who am I" for the hub app's
|
|
// public profile (hub-app theme M1) and, later, federation attestation.
|
|
package instance
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// Identity is the instance's user-visible self-description. Name falls back to
|
|
// DCNTRLIZED_INSTANCE_NAME and then the hostname when instance.json is absent;
|
|
// Title and Description are optional and only ever come from the file.
|
|
type Identity struct {
|
|
Name string `json:"name"`
|
|
Title string `json:"title,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
|
|
// Path returns the identity file location for the given data root.
|
|
func Path(dataRoot string) string {
|
|
return filepath.Join(dataRoot, "instance.json")
|
|
}
|
|
|
|
// Load resolves the instance identity: instance.json when present, else
|
|
// DCNTRLIZED_INSTANCE_NAME, else os.Hostname(). Read-on-demand by design —
|
|
// no daemon state, so an edited file shows up on the next request.
|
|
func Load(dataRoot string) Identity {
|
|
id := defaultIdentity()
|
|
|
|
if data, err := os.ReadFile(Path(dataRoot)); err == nil {
|
|
var fromFile Identity
|
|
if json.Unmarshal(data, &fromFile) == nil && fromFile.Name != "" {
|
|
if fromFile.Title != "" {
|
|
id.Title = fromFile.Title
|
|
}
|
|
if fromFile.Description != "" {
|
|
id.Description = fromFile.Description
|
|
}
|
|
id.Name = fromFile.Name
|
|
}
|
|
}
|
|
return id
|
|
}
|
|
|
|
// Save atomically writes the identity file (tmp + rename), chmod 600 like the
|
|
// rest of Maestro's file stores. The owner-gated edit endpoint that uses this
|
|
// lands with the federation theme; the helper exists so both callers share one
|
|
// write path.
|
|
func Save(dataRoot string, id Identity) error {
|
|
if id.Name == "" {
|
|
return fmt.Errorf("instance name must not be empty")
|
|
}
|
|
data, err := json.MarshalIndent(id, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("marshal instance identity: %w", err)
|
|
}
|
|
path := Path(dataRoot)
|
|
tmp := path + ".tmp"
|
|
if err := os.WriteFile(tmp, append(data, '\n'), 0o600); err != nil {
|
|
return fmt.Errorf("write instance identity: %w", err)
|
|
}
|
|
if err := os.Rename(tmp, path); err != nil {
|
|
return fmt.Errorf("replace instance identity: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func defaultIdentity() Identity {
|
|
name := os.Getenv("DCNTRLIZED_INSTANCE_NAME")
|
|
if name == "" {
|
|
if host, err := os.Hostname(); err == nil {
|
|
name = host
|
|
}
|
|
}
|
|
return Identity{Name: name}
|
|
}
|