Major changes: logic add to panel, local variables, panel histor, users management...
This commit is contained in:
+235
-10
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -23,6 +24,15 @@ type InterfaceMeta struct {
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
// VersionMeta describes a single persisted revision of an interface.
|
||||
type VersionMeta struct {
|
||||
Version int `json:"version"`
|
||||
Name string `json:"name"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Current bool `json:"current"`
|
||||
SavedAt time.Time `json:"savedAt"`
|
||||
}
|
||||
|
||||
// Store persists interface definitions as XML files under {storageDir}/interfaces/
|
||||
// and workspace-level JSON blobs directly in {storageDir}.
|
||||
type Store struct {
|
||||
@@ -123,6 +133,7 @@ type rootAttrs struct {
|
||||
ID string `xml:"id,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
Version int `xml:"version,attr"`
|
||||
Tag string `xml:"tag,attr"`
|
||||
}
|
||||
|
||||
func (s *Store) readMeta(id string) (InterfaceMeta, error) {
|
||||
@@ -151,8 +162,8 @@ func (s *Store) Get(id string) ([]byte, error) {
|
||||
|
||||
// Create stores new interface XML and returns the generated ID.
|
||||
// The ID is derived from the name attribute; a timestamp suffix is added to
|
||||
// ensure uniqueness.
|
||||
func (s *Store) Create(xmlData []byte) (string, error) {
|
||||
// ensure uniqueness. If tag is non-empty it is stamped as the revision label.
|
||||
func (s *Store) Create(xmlData []byte, tag string) (string, error) {
|
||||
var root rootAttrs
|
||||
if err := xml.Unmarshal(xmlData, &root); err != nil {
|
||||
return "", fmt.Errorf("invalid XML: %w", err)
|
||||
@@ -167,18 +178,30 @@ func (s *Store) Create(xmlData []byte) (string, error) {
|
||||
id = id + "-" + fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||
}
|
||||
|
||||
// Initialize version to 1.
|
||||
// Initialize version to 1 and stamp the generated ID so the persisted XML
|
||||
// is self-consistent (the client sends an empty id for new interfaces).
|
||||
updatedData, err := setXMLAttribute(xmlData, "version", "1")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("initialize version attribute: %w", err)
|
||||
}
|
||||
updatedData, err = setXMLAttribute(updatedData, "id", id)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("stamp id attribute: %w", err)
|
||||
}
|
||||
if tag != "" {
|
||||
updatedData, err = setXMLAttribute(updatedData, "tag", tag)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("set tag attribute: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return id, os.WriteFile(s.filePath(id), updatedData, 0o644)
|
||||
}
|
||||
|
||||
// Update replaces the XML for an existing interface, preserving the previous
|
||||
// version as a separate file.
|
||||
func (s *Store) Update(id string, xmlData []byte) error {
|
||||
// version as a separate file. If tag is non-empty it is stamped as the label
|
||||
// for the new revision.
|
||||
func (s *Store) Update(id string, xmlData []byte, tag string) error {
|
||||
if err := validateID(id); err != nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
@@ -213,21 +236,223 @@ func (s *Store) Update(id string, xmlData []byte) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("update version attribute: %w", err)
|
||||
}
|
||||
if tag != "" {
|
||||
updatedData, err = setXMLAttribute(updatedData, "tag", tag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set tag attribute: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return os.WriteFile(oldPath, updatedData, 0o644)
|
||||
}
|
||||
|
||||
// Versions returns metadata for every persisted revision of the interface,
|
||||
// ordered newest-first. The current file is flagged with Current=true.
|
||||
func (s *Store) Versions(id string) ([]VersionMeta, error) {
|
||||
if err := validateID(id); err != nil {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// Current revision.
|
||||
curInfo, err := os.Stat(s.filePath(id))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, ErrNotFound
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
curRoot, err := s.readRoot(s.filePath(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := []VersionMeta{{
|
||||
Version: curRoot.Version,
|
||||
Name: curRoot.Name,
|
||||
Tag: curRoot.Tag,
|
||||
Current: true,
|
||||
SavedAt: curInfo.ModTime(),
|
||||
}}
|
||||
|
||||
// Backup revisions: id.vN.xml
|
||||
entries, err := os.ReadDir(s.dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prefix := id + ".v"
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if e.IsDir() || !strings.HasPrefix(name, prefix) || !strings.HasSuffix(name, ".xml") {
|
||||
continue
|
||||
}
|
||||
if !isVersioned(name) {
|
||||
continue
|
||||
}
|
||||
p := filepath.Join(s.dir, name)
|
||||
info, err := e.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
root, err := s.readRoot(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, VersionMeta{
|
||||
Version: root.Version,
|
||||
Name: root.Name,
|
||||
Tag: root.Tag,
|
||||
SavedAt: info.ModTime(),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Version > out[j].Version })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetVersion returns the raw XML bytes for a specific revision of an interface.
|
||||
func (s *Store) GetVersion(id string, version int) ([]byte, error) {
|
||||
if err := validateID(id); err != nil {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// The current file may hold the requested version.
|
||||
curRoot, err := s.readRoot(s.filePath(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if curRoot.Version == version {
|
||||
return os.ReadFile(s.filePath(id))
|
||||
}
|
||||
|
||||
backupPath := filepath.Join(s.dir, fmt.Sprintf("%s.v%d.xml", id, version))
|
||||
data, err := os.ReadFile(backupPath)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
// versionPath returns the on-disk path for a specific revision, resolving the
|
||||
// current file when version matches the live revision.
|
||||
func (s *Store) versionPath(id string, version int) (string, error) {
|
||||
cur := s.filePath(id)
|
||||
curRoot, err := s.readRoot(cur)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if curRoot.Version == version {
|
||||
return cur, nil
|
||||
}
|
||||
backup := filepath.Join(s.dir, fmt.Sprintf("%s.v%d.xml", id, version))
|
||||
if _, err := os.Stat(backup); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return backup, nil
|
||||
}
|
||||
|
||||
// SetVersionTag sets (or clears, when tag is empty) the label of a specific
|
||||
// revision in place, without creating a new revision.
|
||||
func (s *Store) SetVersionTag(id string, version int, tag string) error {
|
||||
if err := validateID(id); err != nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
path, err := s.versionPath(id, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updated, err := setXMLAttribute(data, "tag", tag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set tag attribute: %w", err)
|
||||
}
|
||||
return os.WriteFile(path, updated, 0o644)
|
||||
}
|
||||
|
||||
// Promote makes a past revision the current one by re-saving its content as a
|
||||
// new revision on top of history. The existing current revision is preserved
|
||||
// as a backup, so promotion is non-destructive.
|
||||
func (s *Store) Promote(id string, version int) error {
|
||||
data, err := s.GetVersion(id, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Update(id, data, fmt.Sprintf("restored from v%d", version))
|
||||
}
|
||||
|
||||
// Fork creates a brand-new interface from the content of a specific revision,
|
||||
// assigning a fresh unique ID and resetting its version to 1.
|
||||
func (s *Store) Fork(id string, version int) (string, error) {
|
||||
data, err := s.GetVersion(id, version)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
newID := id + "-fork-" + fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||
updated, err := setXMLAttribute(data, "version", "1")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reset version attribute: %w", err)
|
||||
}
|
||||
updated, err = setXMLAttribute(updated, "id", newID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("stamp id attribute: %w", err)
|
||||
}
|
||||
updated, err = setXMLAttribute(updated, "tag", "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("clear tag attribute: %w", err)
|
||||
}
|
||||
return newID, os.WriteFile(s.filePath(newID), updated, 0o644)
|
||||
}
|
||||
|
||||
// readRoot parses the top-level interface attributes from the file at path.
|
||||
func (s *Store) readRoot(path string) (rootAttrs, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return rootAttrs{}, err
|
||||
}
|
||||
var root rootAttrs
|
||||
if err := xml.Unmarshal(data, &root); err != nil {
|
||||
return rootAttrs{}, err
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
|
||||
// setXMLAttribute is a primitive helper to update a top-level attribute in an
|
||||
// XML blob without full unmarshal/marshal (which might mess up formatting).
|
||||
func setXMLAttribute(data []byte, attr, value string) ([]byte, error) {
|
||||
// Very basic implementation: look for the attribute in the first 512 bytes.
|
||||
// A proper implementation would use an XML encoder or a better regex.
|
||||
s := string(data)
|
||||
tagEnd := strings.Index(s, ">")
|
||||
if tagEnd == -1 {
|
||||
|
||||
// Locate the root element's opening tag, skipping any XML declaration
|
||||
// (<?xml ... ?>), comments, or processing instructions. Operating on the
|
||||
// first '>' in the document would match the '?>' of the XML declaration and
|
||||
// corrupt its version attribute (producing invalid XML).
|
||||
tagStart := -1
|
||||
for i := 0; i+1 < len(s); i++ {
|
||||
if s[i] == '<' && s[i+1] != '?' && s[i+1] != '!' {
|
||||
tagStart = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if tagStart == -1 {
|
||||
return nil, errors.New("malformed XML: no root element")
|
||||
}
|
||||
rel := strings.Index(s[tagStart:], ">")
|
||||
if rel == -1 {
|
||||
return nil, errors.New("malformed XML: no root tag end")
|
||||
}
|
||||
rootTag := s[:tagEnd]
|
||||
tagEnd := tagStart + rel
|
||||
rootTag := s[tagStart:tagEnd]
|
||||
|
||||
attrSearch := " " + attr + "=\""
|
||||
idx := strings.Index(rootTag, attrSearch)
|
||||
@@ -240,8 +465,8 @@ func setXMLAttribute(data []byte, attr, value string) ([]byte, error) {
|
||||
return []byte(s[:insertIdx] + fmt.Sprintf(" %s=\"%s\"", attr, value) + s[insertIdx:]), nil
|
||||
}
|
||||
|
||||
// Attribute found, replace its value.
|
||||
valStart := idx + len(attrSearch)
|
||||
// Attribute found, replace its value (idx is relative to rootTag).
|
||||
valStart := tagStart + idx + len(attrSearch)
|
||||
valEnd := strings.Index(s[valStart:], "\"")
|
||||
if valEnd == -1 {
|
||||
return nil, errors.New("malformed XML: attribute value not closed")
|
||||
|
||||
Reference in New Issue
Block a user