Wworking on improving the tool
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
@@ -80,10 +81,16 @@ func (s *Store) List() ([]InterfaceMeta, error) {
|
||||
}
|
||||
var out []InterfaceMeta
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".xml") {
|
||||
name := e.Name()
|
||||
if e.IsDir() || !strings.HasSuffix(strings.ToLower(name), ".xml") {
|
||||
continue
|
||||
}
|
||||
id := strings.TrimSuffix(e.Name(), ".xml")
|
||||
// Skip versioned backups: id.v1.xml, id.v2.xml, etc.
|
||||
if isVersioned(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
id := strings.TrimSuffix(name, filepath.Ext(name))
|
||||
meta, err := s.readMeta(id)
|
||||
if err != nil {
|
||||
continue // skip corrupt files silently
|
||||
@@ -96,6 +103,20 @@ func (s *Store) List() ([]InterfaceMeta, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func isVersioned(name string) bool {
|
||||
// Format: <id>.v<number>.xml
|
||||
parts := strings.Split(name, ".")
|
||||
if len(parts) != 3 {
|
||||
return false
|
||||
}
|
||||
vPart := parts[1]
|
||||
if !strings.HasPrefix(vPart, "v") {
|
||||
return false
|
||||
}
|
||||
_, err := strconv.Atoi(vPart[1:])
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// rootAttrs is used to parse only the top-level XML attributes.
|
||||
type rootAttrs struct {
|
||||
XMLName xml.Name `xml:"interface"`
|
||||
@@ -145,18 +166,87 @@ func (s *Store) Create(xmlData []byte) (string, error) {
|
||||
if _, err := os.Stat(s.filePath(id)); err == nil {
|
||||
id = id + "-" + fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||
}
|
||||
return id, os.WriteFile(s.filePath(id), xmlData, 0o644)
|
||||
|
||||
// Initialize version to 1.
|
||||
updatedData, err := setXMLAttribute(xmlData, "version", "1")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("initialize version attribute: %w", err)
|
||||
}
|
||||
|
||||
return id, os.WriteFile(s.filePath(id), updatedData, 0o644)
|
||||
}
|
||||
|
||||
// Update replaces the XML for an existing interface.
|
||||
// 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 {
|
||||
if err := validateID(id); err != nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
if _, err := os.Stat(s.filePath(id)); errors.Is(err, os.ErrNotExist) {
|
||||
return ErrNotFound
|
||||
|
||||
oldPath := s.filePath(id)
|
||||
oldData, err := os.ReadFile(oldPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(s.filePath(id), xmlData, 0o644)
|
||||
|
||||
// Parse current version to name the backup file.
|
||||
var oldRoot rootAttrs
|
||||
if err := xml.Unmarshal(oldData, &oldRoot); err != nil {
|
||||
return fmt.Errorf("parse existing XML: %w", err)
|
||||
}
|
||||
if oldRoot.Version < 1 {
|
||||
oldRoot.Version = 1
|
||||
}
|
||||
|
||||
// Move old file to backup path: id.vN.xml
|
||||
backupPath := filepath.Join(s.dir, fmt.Sprintf("%s.v%d.xml", id, oldRoot.Version))
|
||||
if err := os.WriteFile(backupPath, oldData, 0o644); err != nil {
|
||||
return fmt.Errorf("create backup: %w", err)
|
||||
}
|
||||
|
||||
// Update version in new data.
|
||||
newVersion := oldRoot.Version + 1
|
||||
updatedData, err := setXMLAttribute(xmlData, "version", fmt.Sprintf("%d", newVersion))
|
||||
if err != nil {
|
||||
return fmt.Errorf("update version attribute: %w", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(oldPath, updatedData, 0o644)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, errors.New("malformed XML: no root tag end")
|
||||
}
|
||||
rootTag := s[:tagEnd]
|
||||
|
||||
attrSearch := " " + attr + "=\""
|
||||
idx := strings.Index(rootTag, attrSearch)
|
||||
if idx == -1 {
|
||||
// Attribute not found, insert it before the tag ends.
|
||||
insertIdx := tagEnd
|
||||
if s[tagEnd-1] == '/' {
|
||||
insertIdx-- // handle <tag />
|
||||
}
|
||||
return []byte(s[:insertIdx] + fmt.Sprintf(" %s=\"%s\"", attr, value) + s[insertIdx:]), nil
|
||||
}
|
||||
|
||||
// Attribute found, replace its value.
|
||||
valStart := idx + len(attrSearch)
|
||||
valEnd := strings.Index(s[valStart:], "\"")
|
||||
if valEnd == -1 {
|
||||
return nil, errors.New("malformed XML: attribute value not closed")
|
||||
}
|
||||
return []byte(s[:valStart] + value + s[valStart+valEnd:]), nil
|
||||
}
|
||||
|
||||
// Delete removes the interface with the given ID.
|
||||
|
||||
Reference in New Issue
Block a user