package services import ( "encoding/json" "errors" "fmt" "os" "sort" "time" "git.faercol.me/faercol/http-boot-server/bootserver/bootoption" "git.faercol.me/faercol/http-boot-server/bootserver/filelock" "github.com/google/uuid" "github.com/sirupsen/logrus" ) var ErrUnknownClient = errors.New("unknown client") var ErrUnselectedBootOption = errors.New("unselected boot option") var ErrUnknownBootOption = errors.New("unknown boot option") const defaultLockTimeout = 1 * time.Second type ClientHandlerService struct { filepath string fileLock *filelock.FileLock lockTimeout time.Duration logger *logrus.Logger } func NewClientHandlerService(filepath string, logger *logrus.Logger) *ClientHandlerService { return &ClientHandlerService{ filepath: filepath, fileLock: filelock.New(filepath), lockTimeout: defaultLockTimeout, logger: logger, } } func (chs *ClientHandlerService) Init() { if _, err := os.Open(chs.filepath); errors.Is(err, os.ErrNotExist) { if err := os.WriteFile(chs.filepath, []byte("{}"), 0o644); err != nil { panic(fmt.Errorf("failed to init data file: %w", err)) } } chs.fileLock.Unlock() } func (chs *ClientHandlerService) unload(conf map[uuid.UUID]*bootoption.Client) error { dat, err := json.MarshalIndent(conf, "", "\t") if err != nil { return fmt.Errorf("failed to marshal data to JSON: %w", err) } if err := os.WriteFile(chs.filepath, dat, 0o644); err != nil { return fmt.Errorf("failed to commit to the data file: %w", err) } if err := chs.fileLock.Unlock(); err != nil { return fmt.Errorf("failed to release the lock to data file: %w", err) } return nil } func (chs *ClientHandlerService) load() (map[uuid.UUID]*bootoption.Client, error) { conf := make(map[uuid.UUID]*bootoption.Client) if err := chs.fileLock.Lock(chs.lockTimeout); err != nil { return nil, fmt.Errorf("failed to obtain a lock to the data file: %w", err) } dat, err := os.ReadFile(chs.filepath) if err != nil { defer chs.fileLock.Unlock() return nil, fmt.Errorf("failed to read data file: %w", err) } if err := json.Unmarshal(dat, &conf); err != nil { defer chs.fileLock.Unlock() return nil, fmt.Errorf("failed to parse data file: %w", err) } return conf, nil } func (chs *ClientHandlerService) unloadNoCommmit(conf map[uuid.UUID]*bootoption.Client) { if err := chs.unload(conf); err != nil { chs.logger.Errorf("Failed to unload config: %q", err.Error()) } } func (chs *ClientHandlerService) AddClient(client *bootoption.Client) (uuid.UUID, error) { clients, err := chs.load() if err != nil { return uuid.Nil, fmt.Errorf("failed to load current config: %w", err) } client.ID = uuid.New() clients[client.ID] = client if err := chs.unload(clients); err != nil { return uuid.Nil, fmt.Errorf("failed to save current config: %w", err) } return client.ID, nil } func (chs *ClientHandlerService) GetAllClientConfig() ([]*bootoption.Client, error) { clients, err := chs.load() if err != nil { return nil, fmt.Errorf("failed to load current config %w", err) } defer chs.unloadNoCommmit(clients) clientList := []*bootoption.Client{} for id, clt := range clients { clt.ID = id clientList = append(clientList, clt) } sort.Slice(clientList, func(i, j int) bool { return clientList[i].ID.String() < clientList[j].ID.String() }) return clientList, nil } func (chs *ClientHandlerService) GetClientConfig(client uuid.UUID) (*bootoption.Client, error) { clients, err := chs.load() if err != nil { return nil, fmt.Errorf("failed to load current config %w", err) } defer chs.unloadNoCommmit(clients) clientDetails, ok := clients[client] if !ok { return nil, ErrUnknownClient } clientDetails.ID = client return clientDetails, nil } func (chs *ClientHandlerService) GetClientSelectedBootOption(client uuid.UUID) (*bootoption.EFIApp, error) { clients, err := chs.load() if err != nil { return nil, fmt.Errorf("failed to load current config: %w", err) } defer chs.unloadNoCommmit(clients) clientDetails, ok := clients[client] if !ok { return nil, ErrUnknownClient } if clientDetails.SelectedOption == "" { return nil, ErrUnselectedBootOption } if option, ok := clientDetails.Options[clientDetails.SelectedOption]; !ok { return nil, ErrUnknownBootOption } else { return &option, nil } } func (chs *ClientHandlerService) DeleteClient(client uuid.UUID) error { var err error clients, err := chs.load() if err != nil { return fmt.Errorf("failed to load current config: %w", err) } delete(clients, client) if err := chs.unload(clients); err != nil { return fmt.Errorf("failed to save current config: %w", err) } return nil } func (chs *ClientHandlerService) SetClientBootOption(client uuid.UUID, option string) error { var err error clients, err := chs.load() if err != nil { return fmt.Errorf("failed to load current config: %w", err) } clientDetails, ok := clients[client] if !ok { err = ErrUnknownClient } else { if _, ok := clientDetails.Options[option]; !ok { err = ErrUnknownBootOption } else { clientDetails.SelectedOption = option } } if err != nil { defer chs.unloadNoCommmit(clients) return err } if err := chs.unload(clients); err != nil { return fmt.Errorf("failed to save current config: %w", err) } return nil }