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 ¶
var ErrNotFound = errors.New("recording: not found")
ErrNotFound is returned by OpenCast when no recording exists for the run.
Functions ¶
func CastKey ¶
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 ¶
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 ¶
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 ¶
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 ¶
NewFSStore returns an FSStore that persists casts under root. The directory is created with mode 0o750 if it does not exist.
func (*FSStore) OpenCast ¶
OpenCast opens <root>/<runID>.cast for reading. Returns ErrNotFound when the file does not exist.
func (*FSStore) SaveCast ¶
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 ¶
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.
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. |