Files
sendico/api/billing/documents/internal/docstore/local.go
Stephan D e77d1ab793 linting
2026-03-10 12:31:09 +01:00

110 lines
2.6 KiB
Go

package docstore
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
type LocalStore struct {
logger mlogger.Logger
rootPath string
}
func NewLocalStore(logger mlogger.Logger, cfg LocalConfig) (*LocalStore, error) {
root := strings.TrimSpace(cfg.RootPath)
if root == "" {
return nil, merrors.InvalidArgument("docstore: local root_path is empty")
}
store := &LocalStore{
logger: logger.Named("docstore").Named("local"),
rootPath: root,
}
store.logger.Info("Document storage initialised", zap.String("root_path", root))
return store, nil
}
func (s *LocalStore) Save(ctx context.Context, key string, data []byte) error {
if err := ctx.Err(); err != nil {
return err
}
path, err := resolveStoragePath(s.rootPath, key)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
s.logger.Warn("Failed to create document directory", zap.Error(err), zap.String("path", path))
return err
}
if err := os.WriteFile(path, data, 0o600); err != nil {
s.logger.Warn("Failed to write document file", zap.Error(err), zap.String("path", path))
return err
}
return nil
}
func (s *LocalStore) Load(ctx context.Context, key string) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
path, err := resolveStoragePath(s.rootPath, key)
if err != nil {
return nil, err
}
//nolint:gosec // path is constrained by resolveStoragePath to stay within configured root.
data, err := os.ReadFile(path)
if err != nil {
s.logger.Warn("Failed to read document file", zap.Error(err), zap.String("path", path))
return nil, err
}
return data, nil
}
var _ Store = (*LocalStore)(nil)
func resolveStoragePath(rootPath string, key string) (string, error) {
cleanKey := filepath.Clean(strings.TrimSpace(key))
if cleanKey == "" || cleanKey == "." {
return "", merrors.InvalidArgument("docstore: key is required")
}
if filepath.IsAbs(cleanKey) {
return "", merrors.InvalidArgument("docstore: absolute keys are not allowed")
}
rootAbs, err := filepath.Abs(rootPath)
if err != nil {
return "", fmt.Errorf("resolve local store root: %w", err)
}
path := filepath.Join(rootAbs, cleanKey)
pathAbs, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("resolve local store path: %w", err)
}
prefix := rootAbs + string(filepath.Separator)
if pathAbs != rootAbs && !strings.HasPrefix(pathAbs, prefix) {
return "", merrors.InvalidArgument("docstore: key escapes root path")
}
return pathAbs, nil
}