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
- func CommandSlug(command string) string
- func Component(s string) string
- func FileName(t time.Time, command string) string
- func ParseFileName(name string) (time.Time, string, bool)
- type Entry
- type Filter
- type Record
- type Ref
- type Store
- func (s *Store) Dir(ref Ref) string
- func (s *Store) List(f Filter) ([]Entry, error)
- func (s *Store) Plan(ref Ref, command string, t time.Time) string
- func (s *Store) ProfileDir(profile string) string
- func (s *Store) Prune(f Filter, dryRun bool) ([]Entry, error)
- func (s *Store) Writable(profile string) error
- func (s *Store) Write(rec *Record) (string, error)
Constants ¶
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 ¶
CommandSlug turns "globals update" into "globals-update".
func Component ¶
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.
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 ¶
Load reads one backup file. Anything that is not a PayCLI backup of a version this build understands is refused by name.
func Parse ¶
Parse decodes a backup file's bytes. Numbers stay json.Number so an id or a numeric field round-trips exactly.
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 (*Store) List ¶
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 ¶
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 ¶
ProfileDir is the directory one profile's backups live in.
func (*Store) Prune ¶
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.