Files

134 lines
3.4 KiB
Go

package store
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"linkport/internal/model"
"sort"
)
type ImportResource struct {
Kind string `json:"kind"`
ID string `json:"id"`
Action string `json:"action"`
Changes []ConfigChange `json:"changes"`
}
type ImportPreview struct {
Scopes *model.ScopeCatalog `json:"scopes,omitempty"`
Assets int `json:"assets"`
Revision string `json:"revision"`
Version int `json:"version"`
Targets int `json:"targets"`
Gateways int `json:"gateways"`
Outbounds int `json:"outbounds"`
Resources []ImportResource `json:"resources"`
}
func bundleResources(b Bundle) map[string]any {
out := map[string]any{}
for _, v := range b.Assets {
out["assets/"+v.ID] = v
}
for _, v := range b.Targets {
out["targets/"+v.ID] = v
}
for _, v := range b.Gateways {
out["gateways/"+v.ID] = v
}
for _, v := range b.Outbounds {
out["outbounds/"+v.ID] = v
}
return out
}
func jsonValue(v any) any {
raw, _ := json.Marshal(v)
var out any
_ = json.Unmarshal(raw, &out)
return out
}
// Preview shares merge/validation with RestoreBundle, including existing references.
func (s *Store) PreviewBundle(in Bundle) (ImportPreview, error) {
out := ImportPreview{Version: in.Version, Assets: len(in.Assets), Targets: len(in.Targets), Gateways: len(in.Gateways), Outbounds: len(in.Outbounds), Resources: []ImportResource{}}
tx, err := s.db.Begin()
if err != nil {
return out, err
}
defer tx.Rollback()
current, err := s.bundle(tx)
if err != nil {
return out, err
}
out.Revision, err = configurationRevision(tx)
if err != nil {
return out, err
}
merged, _, err := mergeBundle(current, in)
if err != nil {
return out, err
}
if in.Scopes != nil {
out.Scopes = merged.Scopes
}
before, after := bundleResources(current), bundleResources(merged)
for key := range bundleResources(in) {
kind, id := "", ""
for i, c := range key {
if c == '/' {
kind, id = key[:i], key[i+1:]
break
}
}
changes := compareConfig(jsonValue(before[key]), jsonValue(after[key]))
action := "create"
if _, exists := before[key]; exists {
action = "replace"
if len(changes) == 0 {
action = "unchanged"
}
}
out.Resources = append(out.Resources, ImportResource{Kind: kind, ID: id, Action: action, Changes: changes})
}
sort.Slice(out.Resources, func(i, j int) bool {
return out.Resources[i].Kind+out.Resources[i].ID < out.Resources[j].Kind+out.Resources[j].ID
})
return out, nil
}
func configurationRevision(tx *sql.Tx) (string, error) {
h := sha256.New()
for _, kind := range []string{"assets", "gateways", "targets", "outbounds"} {
rows, err := tx.Query("SELECT id,config FROM " + kind + " ORDER BY id")
if err != nil {
return "", err
}
for rows.Next() {
var id string
var blob []byte
if err = rows.Scan(&id, &blob); err != nil {
rows.Close()
return "", err
}
raw, _ := json.Marshal([]any{kind, id, blob})
h.Write(raw)
}
err = rows.Err()
rows.Close()
if err != nil {
return "", err
}
}
var scopes []byte
err := tx.QueryRow("SELECT config FROM operational_settings WHERE id='scopes'").Scan(&scopes)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return "", err
}
if err == nil {
h.Write(scopes)
}
return hex.EncodeToString(h.Sum(nil)), nil
}