134 lines
3.4 KiB
Go
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
|
|
}
|