backup

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Sep 28, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package backup implements §12.8: the automatic pre-write backup of every document a write is about to change or remove.

A backup is one JSON file per document per write:

<dir>/<profile>/<collection>/<id>/<UTC-timestamp>--<command>.json
<dir>/<profile>/_globals/<slug>/<UTC-timestamp>--<command>.json

holding a Record — the document exactly as the server returned it (depth=0, draft=true where drafts exist) plus the facts needed to put it back: profile, redacted base URL, collection or global, id, locale and the command that was about to overwrite it.

This package owns the on-disk format only. It performs no HTTP and never imports internal/cli: the command layer reads the documents and hands them here, which keeps the format testable without a server and lets every write path share one writer. Every file goes through fsatomic.Write (§3.1) with mode 0600 inside 0700 directories, because a document body is exactly as sensitive as the CMS it came from.

Index

Constants

View Source
const (
	// FormatVersion is Record.PayBackup. A reader refuses any other value
	// rather than guessing at a future layout.
	FormatVersion = 1
	// FilePerm is the mode of every backup file.
	FilePerm fs.FileMode = 0o600
	// GlobalsDir is the directory globals live under, beside the collection
	// directories. The leading underscore cannot collide with a Payload
	// collection slug that PayCLI would ever address.
	GlobalsDir = "_globals"
	// TimeLayout is the UTC timestamp at the front of every file name:
	// lexically sortable, millisecond resolution and free of ':' so the same
	// name is legal on Windows.
	TimeLayout = "20060102T150405.000Z"
	// Ext is the file extension.
	Ext = ".json"
)

Variables

This section is empty.

Functions

func CommandSlug

func CommandSlug(command string) string

CommandSlug turns "globals update" into "globals-update".

func Component

func Component(s string) string

Component makes one path element safe on every platform: anything outside [A-Za-z0-9._-] becomes '_', and the two names a path element can never be ("." and "..") are prefixed. Payload slugs and numeric, ObjectID or UUID ids pass through unchanged.

func FileName

func FileName(t time.Time, command string) string

FileName is "<UTC-timestamp>--<command>.json".

func ParseFileName

func ParseFileName(name string) (time.Time, string, bool)

ParseFileName splits "<timestamp>--<command>[.N].json".

Types

type Entry

type Entry struct {
	Path       string    `json:"path"`
	Profile    string    `json:"profile"`
	Collection string    `json:"collection,omitempty"`
	Global     string    `json:"global,omitempty"`
	ID         string    `json:"id,omitempty"`
	Command    string    `json:"command"`
	Time       time.Time `json:"time"`
	Bytes      int64     `json:"bytes"`
}

Entry is one backup file as `pay backups list` reports it. Everything is derived from the path, so listing thousands of backups reads no file.

type Filter

type Filter struct {
	// Profile limits the walk to one profile's directory. Empty walks every
	// profile under the root.
	Profile    string
	Collection string
	Global     string
	ID         string
	Command    string
	// Since keeps entries at or after this instant; Before keeps entries
	// strictly before it. Zero means unbounded.
	Since  time.Time
	Before time.Time
}

Filter narrows List and Prune. Empty fields match everything.

type Record

type Record struct {
	// PayBackup is the format marker and version; always FormatVersion.
	PayBackup int `json:"pay_backup"`
	// Time is when the document was read, UTC.
	Time time.Time `json:"time"`
	// Profile and BaseURL say where the document lives. BaseURL is redacted
	// (§5.3) before it is written: it is echoed into every file.
	Profile string `json:"profile"`
	BaseURL string `json:"base_url"`
	// Exactly one of Collection and Global is set.
	Collection string `json:"collection,omitempty"`
	Global     string `json:"global,omitempty"`
	// ID is the document id exactly as the server returned it (a JSON number
	// stays a number). Absent for a global.
	ID any `json:"id,omitempty"`
	// Command is the write that was about to run ("update", "delete", …).
	Command string `json:"command"`
	// RequestID is the X-Request-Id of the read that captured Doc, so the
	// capture can be found in the server log.
	RequestID string `json:"request_id"`
	// Draft is true when Doc was read with draft=true (the collection has
	// drafts): Doc is then the newest version, which may be an unpublished
	// draft on top of a published document.
	Draft bool `json:"draft"`
	// Trash is true when Doc was read with trash=true.
	Trash bool `json:"trash,omitempty"`
	// Locale and FallbackLocale are the locale parameters the capture used,
	// which are the ones the write was about to use. A restore sends them
	// back so it writes the same locale it read.
	Locale         string `json:"locale,omitempty"`
	FallbackLocale string `json:"fallback_locale,omitempty"`
	// RedactedPaths lists the values §5.3 masked before the file was written
	// (an apiKey on a users document, anything named *token* or *secret*).
	// A restore never writes those paths back.
	RedactedPaths []string `json:"redacted_paths,omitempty"`
	// CLIVersion is the PayCLI build that wrote the file.
	CLIVersion string `json:"cli_version,omitempty"`
	// Doc is the document.
	Doc map[string]any `json:"doc"`
	// PublishedDoc is the document's main row — its live, published state —
	// captured only when it differs from Doc because a newer draft sits on
	// top of a published document (Doc's _status is "draft", the main row's
	// "published"). That published state is exactly what a non-draft write
	// (update without --draft, publish, apply, upsert, sync) overwrites, so a
	// restore writes it back first and then Doc as the draft on top.
	PublishedDoc map[string]any `json:"published_doc,omitempty"`
}

Record is one backup file.

func Load

func Load(path string) (*Record, error)

Load reads one backup file. Anything that is not a PayCLI backup of a version this build understands is refused by name.

func Parse

func Parse(data []byte, path string) (*Record, error)

Parse decodes a backup file's bytes. Numbers stay json.Number so an id or a numeric field round-trips exactly.

func (*Record) IDString

func (r *Record) IDString() string

IDString is the id as text, "" for a global.

func (*Record) Kind

func (r *Record) Kind() string

Kind reports "global" or "collection".

func (*Record) Slug

func (r *Record) Slug() string

Slug is the collection or global slug.

type Ref

type Ref struct {
	Profile    string
	Collection string
	Global     string
	ID         string
}

Ref names the document a backup belongs to, which is what decides its directory.

type Store

type Store struct {
	// Root is the configured backup_dir (absolute). Profiles are
	// sub-directories of it.
	Root string
}

Store is a backup directory.

func New

func New(dir string) *Store

New returns a store rooted at dir.

func (*Store) Dir

func (s *Store) Dir(ref Ref) string

Dir is the directory one document's backups live in.

func (*Store) List

func (s *Store) List(f Filter) ([]Entry, error)

List returns every backup matching f, newest first. A root that does not exist yet is an empty list, not an error: no write has needed it so far.

func (*Store) Plan

func (s *Store) Plan(ref Ref, command string, t time.Time) string

Plan is the path a backup of ref taken at t by command would be written to. It is what `--dry-run` reports as would_backup; Write may add a ".N" suffix when that exact name is already taken.

func (*Store) ProfileDir

func (s *Store) ProfileDir(profile string) string

ProfileDir is the directory one profile's backups live in.

func (*Store) Prune

func (s *Store) Prune(f Filter, dryRun bool) ([]Entry, error)

Prune removes every backup matching f (normally Filter.Before) and then any directory the removal left empty. With dryRun it only reports what it would remove. It returns the removed (or would-be removed) entries.

func (*Store) Writable

func (s *Store) Writable(profile string) error

Writable reports whether a backup could be written for profile right now. It creates the profile directory — which the first real backup would do anyway — and a probe file that it removes again.

func (*Store) Write

func (s *Store) Write(rec *Record) (string, error)

Write stores rec and returns the file's path. The name is unique: a second backup of the same document in the same millisecond gets a ".2" suffix rather than replacing the first.

Jump to

Keyboard shortcuts

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