Files
mike 1a39f54858 ipadm: translate to English, fix help alignment
- All ipadm-internal messages (usage, prompts, errors) are now English
- Usage text is built programmatically from a (left, desc) row list with
  computed column width instead of hand-aligned spaces, so it can't drift
  out of alignment again when rows are added/changed
- README's literal usage block updated to match; rest of the repo docs
  stay German
2026-08-18 11:49:47 +02:00

93 lines
2.2 KiB
Go

package main
import (
"bufio"
"fmt"
"os"
"strings"
"syscall"
)
// LineStore gives locked read-modify-write access to a flat text file with
// one record per line. Lines starting with '#' and blank lines are ignored.
// parse/format convert between a line and a record of type T.
type LineStore[T any] struct {
path string
file *os.File
header string
parse func(line string) (T, error)
format func(v T) string
}
func openLineStore[T any](path, header string, parse func(string) (T, error), format func(T) string) (*LineStore[T], error) {
if err := os.MkdirAll(dirOf(path), 0755); err != nil {
return nil, fmt.Errorf("cannot create directory for %s: %w", path, err)
}
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return nil, fmt.Errorf("cannot open %s: %w", path, err)
}
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
f.Close()
return nil, fmt.Errorf("cannot lock %s: %w", path, err)
}
return &LineStore[T]{path: path, file: f, header: header, parse: parse, format: format}, nil
}
func (s *LineStore[T]) Close() error {
syscall.Flock(int(s.file.Fd()), syscall.LOCK_UN)
return s.file.Close()
}
func (s *LineStore[T]) Load() ([]T, error) {
if _, err := s.file.Seek(0, 0); err != nil {
return nil, err
}
var items []T
sc := bufio.NewScanner(s.file)
lineNo := 0
for sc.Scan() {
lineNo++
line := strings.TrimRight(sc.Text(), "\r\n")
if line == "" || strings.HasPrefix(line, "#") {
continue
}
v, err := s.parse(line)
if err != nil {
return nil, fmt.Errorf("%s:%d: %w", s.path, lineNo, err)
}
items = append(items, v)
}
if err := sc.Err(); err != nil {
return nil, err
}
return items, nil
}
func (s *LineStore[T]) Save(items []T) error {
var b strings.Builder
b.WriteString(s.header)
for _, v := range items {
b.WriteString(s.format(v))
b.WriteString("\n")
}
if err := s.file.Truncate(0); err != nil {
return err
}
if _, err := s.file.Seek(0, 0); err != nil {
return err
}
if _, err := s.file.WriteString(b.String()); err != nil {
return err
}
return s.file.Sync()
}
func dirOf(path string) string {
i := strings.LastIndexByte(path, '/')
if i <= 0 {
return "."
}
return path[:i]
}