recording

package
v0.4.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package recording provides storage and HTTP serving of asciicast session recordings produced by wardyn-rec. The Store interface is intentionally minimal so the fs-backed implementation can later be replaced by object storage without touching callers.

Security constraints:

  • All path construction goes through safeRunPath, which rejects any runID containing path separators or dot-sequences (path-traversal prevention).
  • OpenCast returns (nil, ErrNotFound) for absent recordings so callers can distinguish "never recorded" from storage errors.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("recording: not found")

ErrNotFound is returned by OpenCast when no recording exists for the run.

Functions

func CastKey

func CastKey(runID, suffix string) string

CastKey builds the composite cast key for a run + optional session suffix. An empty suffix yields the bare runID (the batch-run cast key).

func Handler

func Handler(store Store) http.Handler

Handler returns an http.Handler that serves GET /{runID} as an asciicast stream (Content-Type: application/x-asciicast). Mount it under /api/v1/runs/{id}/recording in the wardynd router.

The {runID} URL parameter is extracted via chi. A 404 is returned when no recording exists for that run. Errors from the store produce 500.

func Names

func Names() []string

Names returns the registered store names (for /healthz and error messages).

func Register

func Register(name string, c Constructor)

Register adds a recording-store implementation; call it from an init().

Types

type CastHeader

type CastHeader struct {
	Version   int   `json:"version"`
	Width     int   `json:"width"`
	Height    int   `json:"height"`
	Timestamp int64 `json:"timestamp,omitempty"`
}

CastHeader is the asciicast v2 header (line 1 of the stream).

type CastWriter

type CastWriter struct {
	// contains filtered or unexported fields
}

asciicast v2 format: a header object on line 1, then one JSON event array per line. An output event is ["<elapsed seconds>", "o", "<data>"]. Reference: https://docs.asciinema.org/manual/asciicast/v2/

CastWriter incrementally serializes an asciicast v2 stream. It is used to record an interactive attach session: the header is written once at Start, then each chunk of PTY OUTPUT (server->client bytes, already secret-masked by the caller) is appended as a timed "o" event via Write. The serialized bytes accumulate in the wrapped io.Writer (e.g. a bytes.Buffer) so the whole cast can be persisted to the RecordingStore when the session ends.

CastWriter is safe for concurrent use by a single producer goroutine; callers that write from one goroutine (the attach Read pump) and read the buffer from another after Close need no extra locking beyond the internal mutex here.

func NewCastWriter

func NewCastWriter(dst io.Writer, width, height int, startedAt time.Time) *CastWriter

NewCastWriter returns a CastWriter that serializes events into dst. width and height are the initial terminal size recorded in the header (0 values fall back to a sane 80x24 so the replay player has a valid geometry). startedAt is the wall-clock session start; event timestamps are elapsed seconds from it.

func (*CastWriter) HadOutput

func (w *CastWriter) HadOutput() bool

HadOutput reports whether any output event was recorded (beyond the header).

func (*CastWriter) Write

func (w *CastWriter) Write(p []byte) (int, error)

Write appends p as a timed asciicast OUTPUT event ["t","o",string(p)]. The elapsed time is computed from the writer's start time. p is the (already masked) terminal output. It satisfies io.Writer so it can sit directly behind a secretmask.MaskingWriter. A zero-length write is a no-op.

type Constructor

type Constructor func(Deps) (Store, error)

Constructor builds a Store from Deps. It may return (nil, nil) to mean "recording disabled" (the fs store with an empty Dir), which callers treat as no-recording.

type Deps

type Deps struct {
	// Dir is the base directory for filesystem-backed stores. Empty => recording
	// disabled (the fs constructor returns a nil Store).
	Dir string
}

Deps are the platform primitives a recording.Store constructor may use. New seams keep their own typed Deps so heterogeneous construction stays type-safe.

type FSStore

type FSStore struct {
	// contains filtered or unexported fields
}

FSStore is a filesystem-backed Store. Each recording is stored as <root>/<runID>.cast. The root directory is created on first use.

func NewFSStore

func NewFSStore(root string) (*FSStore, error)

NewFSStore returns an FSStore that persists casts under root. The directory is created with mode 0o750 if it does not exist.

func (*FSStore) OpenCast

func (s *FSStore) OpenCast(_ context.Context, runID string) (io.ReadCloser, error)

OpenCast opens <root>/<runID>.cast for reading. Returns ErrNotFound when the file does not exist.

func (*FSStore) SaveCast

func (s *FSStore) SaveCast(_ context.Context, runID string, r io.Reader) error

SaveCast writes the asciicast stream to <root>/<runID>.cast atomically (write to a temp file then rename). Fails closed on any path-traversal attempt.

func (*FSStore) SaveCastNamed

func (s *FSStore) SaveCastNamed(ctx context.Context, runID, suffix string, r io.Reader) error

SaveCastNamed writes the asciicast stream to <root>/<runID>~<suffix>.cast atomically. An empty suffix is equivalent to SaveCast (bare runID key). Both the runID and the composite key are checked by safeRunPath (fails closed on any path-traversal attempt in either component).

type Store

type Store interface {
	SaveCast(ctx context.Context, runID string, r io.Reader) error
	SaveCastNamed(ctx context.Context, runID, suffix string, r io.Reader) error
	OpenCast(ctx context.Context, key string) (io.ReadCloser, error)
}

Store is the recording persistence contract.

SaveCast persists the asciicast bytestream from r under runID, replacing any prior recording for that run. It must be safe for concurrent saves of different runIDs.

SaveCastNamed persists the asciicast bytestream from r under a composite key "<runID>~<suffix>" (e.g. an interactive attach session id), so an interactive session recording does NOT clobber the batch run's cast (keyed by bare runID) and concurrent/sequential attaches each get their own cast. The same path guardrails (no traversal) apply to both runID and suffix. The composite key is what OpenCast surfaces; the recording HTTP handler can serve it by that key. Passing an empty suffix is equivalent to SaveCast.

OpenCast returns a ReadCloser for the asciicast. The caller is responsible for closing it. Returns ErrNotFound when no recording exists. The key is either a bare runID (batch cast) or a "<runID>~<suffix>" composite.

func New

func New(name string, d Deps) (Store, error)

New constructs the recording store selected by name (empty => default).

Directories

Path Synopsis
Package recordingtest provides a reusable conformance suite for any recording.Store implementation.
Package recordingtest provides a reusable conformance suite for any recording.Store implementation.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL