Files
narratio/internal/artifacts/remote_object_key.go

61 lines
2.5 KiB
Go

package artifacts
import (
"fmt"
"strings"
"gitea.maximumdirect.net/eric/narratio/internal/config"
)
// ImmutableRunObjectKey identifies one clean object beneath a session's
// immutable run namespace.
type ImmutableRunObjectKey struct {
RunID string
RelativePath string
}
// ParseImmutableRunObjectKey proves that key belongs to one immutable run in
// the expected campaign/session namespace and returns its run and relative path.
func ParseImmutableRunObjectKey(sessionPrefix, key string) (ImmutableRunObjectKey, error) {
if err := validateRemoteSessionPrefix(sessionPrefix); err != nil {
return ImmutableRunObjectKey{}, err
}
if err := validateRemoteObjectKey(key); err != nil {
return ImmutableRunObjectKey{}, fmt.Errorf("immutable object key %q: %w", key, err)
}
runsPrefix := sessionPrefix + config.S3RunsSegment + "/"
remainder, ok := strings.CutPrefix(key, runsPrefix)
if !ok {
return ImmutableRunObjectKey{}, fmt.Errorf("immutable object key %q is outside session run prefix %q", key, runsPrefix)
}
runID, relativePath, ok := strings.Cut(remainder, "/")
if !ok || runID == "" {
return ImmutableRunObjectKey{}, fmt.Errorf("immutable object key %q has an empty run identity", key)
}
if err := ValidateRunIdentity(runID); err != nil {
return ImmutableRunObjectKey{}, fmt.Errorf("immutable object key %q: %w", key, err)
}
if err := validateRemoteObjectKey(relativePath); err != nil {
return ImmutableRunObjectKey{}, fmt.Errorf("immutable object key %q has invalid run-relative path: %w", key, err)
}
return ImmutableRunObjectKey{RunID: runID, RelativePath: relativePath}, nil
}
func validateRemoteSessionPrefix(sessionPrefix string) error {
if sessionPrefix == "" || strings.TrimSpace(sessionPrefix) != sessionPrefix || strings.Contains(sessionPrefix, `\`) || !strings.HasSuffix(sessionPrefix, "/") {
return fmt.Errorf("session prefix %q must be a canonical relative object prefix", sessionPrefix)
}
withoutSlash := strings.TrimSuffix(sessionPrefix, "/")
if err := validateRemoteObjectKey(withoutSlash); err != nil {
return fmt.Errorf("session prefix %q: %w", sessionPrefix, err)
}
parts := strings.Split(withoutSlash, "/")
if len(parts) < 4 || parts[len(parts)-4] != config.S3CampaignsSegment || parts[len(parts)-2] != config.S3SessionsSegment {
return fmt.Errorf("session prefix %q does not identify a campaign session", sessionPrefix)
}
if err := ValidateSessionIdentity(parts[len(parts)-3], parts[len(parts)-1]); err != nil {
return fmt.Errorf("session prefix %q: %w", sessionPrefix, err)
}
return nil
}