versions

package
v1.4.1 Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package versions stores plain, uncompressed backups of malleable HTML files.

Layout:

<UserConfigDir>/htmlclay/versions/            0700
    notes-a3f19c2b/                           <basename>-<8 hex display suffix>
        2026-07-19-14-22-08-431-0400.html     0600, uncompressed, local + offset
        meta.json                             {name, absPath, key, updatedAt}

A version filename carries LOCAL wall time so it reads correctly in a file browser, followed by the signed UTC offset that was in force at that moment. The offset is what makes the name a single instant: local wall time repeats for one hour on every DST fall-back, and this is a delete path, so a name that cannot be ordered is a name that can cost the user the version they wanted.

The 8 hex characters in the folder name are a display affordance only. The logical key is the full UUID or the full path hash, recorded in meta.json, and two distinct keys sharing a display prefix never share a history.

Containment is structural, not lexical. Every mutation runs through an *os.Root opened for the duration of one locked operation: the versions directory is acquired without trusting its final component, each history is reopened with an Lstat + OpenRoot + os.SameFile check, and deletes, links, renames and reads all go through the opened root. A history directory swapped for a symlink between a path check and a syscall can therefore no longer redirect a delete outside the store.

Index

Constants

View Source
const (
	// MaxAge is the age past which a version is eligible for deletion.
	MaxAge = 60 * 24 * time.Hour
	// MinKeep is the number of newest versions always retained per history,
	// regardless of age. Pruning retains the union of the two rules.
	MinKeep = 20

	// ProvisionalMaxAge is how long a first-open snapshot taken under a freshly
	// minted identity survives when no save ever makes it durable. Past this,
	// PruneAll deletes the whole history at startup, so opening a file and never
	// saving it does not leak a copy forever.
	ProvisionalMaxAge = 7 * 24 * time.Hour
)

Variables

View Source
var (
	// ErrNoHistory is returned when a key has no history folder on disk.
	ErrNoHistory = errors.New("no version history for this file")
	// ErrBadName is returned when a version name is not exactly one generated
	// filename.
	ErrBadName = errors.New("invalid version name")
	// ErrNotRegular is returned when a version path is not a regular file.
	ErrNotRegular = errors.New("version is not a regular file")
)

Functions

func Hash

func Hash(data []byte) string

Hash returns the hex sha256 of data, the form used for both per-file records.

func IDFromKey

func IDFromKey(key string) (string, bool)

IDFromKey returns the canonical UUID an id: history key carries, or false for a path: key. It is the inverse of the id: half of Key, so restore can honor the stored identity without re-parsing the key grammar inline.

func IsCanonicalUUID

func IsCanonicalUUID(id string) bool

IsCanonicalUUID reports whether id is a canonical 8-4-4-4-12 hex UUID. ReadHTMLClayID returns whatever is in the attribute unvalidated, so a hand-edited file can carry `..` or a short string; only a canonical id is trusted as a history key.

func Key

func Key(absPath string, data []byte) string

Key returns the logical history key for a file.

A .htmlclay file keys by its htmlclayid, but only after that id validates as a canonical UUID. A plain .html file never receives an injected id, so it keys by a hash of its absolute path. That key does not survive a move; the same fallback applies to a .htmlclay file carrying an invalid id.

func ParseEntryName

func ParseEntryName(name string) (time.Time, int, error)

ParseEntryName validates a version filename and returns its ordering key. It accepts exactly one generated filename and nothing else.

func SyncDir

func SyncDir(dir string) error

SyncDir fsyncs a directory so a rename or link into it is durable. Writing and fsyncing the temp file only makes its contents durable; without this the directory entry that gives those bytes their final name can still be lost, so an acknowledged save may not survive power loss.

A filesystem that does not support fsync on a directory reports EINVAL or ENOTSUP. That is not a durability failure the caller can act on, so it is treated as success rather than failing the write it protects.

Types

type ClaimStatus

type ClaimStatus int

ClaimStatus is the outcome of Claim.

const (
	// ClaimOwned means absPath now owns key: it was unowned, or already bound
	// here. Nothing further is required of the caller.
	ClaimOwned ClaimStatus = iota
	// ClaimRenamed means key was bound to a different path that is definitively
	// gone, so this is a rename. The history has been rebound to absPath.
	ClaimRenamed
	// ClaimClone means key is still owned by a different path, so absPath is a
	// copy and the caller must fork it a fresh identity.
	ClaimClone
)

type Entry

type Entry struct {
	Name string    `json:"name"`
	Time time.Time `json:"time"`
	Seq  int       `json:"seq"`
	Size int64     `json:"size"`
}

Entry is one stored version.

type Store

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

Store owns the versions directory. Every exported method takes the store lock. The lock hierarchy is one rule: callers acquire session.File's lock BEFORE the store lock, never the reverse.

func New

func New(baseDir string) *Store

New returns a store rooted at baseDir, which is created lazily on first write.

func (*Store) Backup

func (s *Store) Backup(key, absPath string, content []byte) (bool, error)

Backup publishes content as a new version of key's history, atomically. It reports whether a new version was actually written: identical content is deduped and reports false with a nil error.

func (*Store) BaseDir

func (s *Store) BaseDir() string

BaseDir returns the resolved versions directory without creating it.

func (*Store) BoundPath

func (s *Store) BoundPath(key string) (string, bool)

BoundPath returns the absolute path a key's history is currently bound to.

func (*Store) Claim

func (s *Store) Claim(key, absPath string) (ClaimStatus, string, error)

Claim checks and claims ownership of key for absPath in one store transaction.

Checking ownership and claiming it in two transactions let two copies of one file, first-opened concurrently, both see no owner: neither got a fresh id and both landed in a single logical history, whose folder then rebound to whichever ran last. Reserving the key here, before any folder exists, makes the second caller see an owner.

Only a definitive not-exists means the old path is gone. Any other Lstat error, EACCES above all, means "still there", so a transient permission failure cannot rebind a history onto a clone.

func (*Store) Contains

func (s *Store) Contains(absPath string) bool

Contains reports whether absPath sits inside the versions directory. The server uses it to deny requests for internal backup state on the app's own origin: the config directory lives under the user's home on every platform, so the static path would otherwise be reachable.

func (*Store) Dir

func (s *Store) Dir() (string, error)

Dir returns the versions directory, creating it if needed. Used by the tray item so "Backups" opens something that exists.

func (*Store) HasHistory

func (s *Store) HasHistory(key, absPath string) bool

HasHistory reports whether key already has at least one stored version. Used to decide whether a save is the first one for a file, which is when the existing on-disk content is versioned so the pre-Hyperclay state survives.

func (*Store) List

func (s *Store) List(key, absPath string) ([]Entry, error)

List returns key's versions, newest first.

func (*Store) MaybePrune

func (s *Store) MaybePrune(key, absPath string)

MaybePrune prunes key's history at most once per hour. Called opportunistically after a successful backup, on the store lock only, never inside the file lock.

func (*Store) PruneAll

func (s *Store) PruneAll()

PruneAll prunes every history, including folders left behind by a failed rename. Called once at startup. A history whose folder is a symlink or is otherwise not a real directory is refused, deleting nothing.

func (*Store) Read

func (s *Store) Read(key, absPath, name string) ([]byte, error)

Read returns the bytes of one version. name must be exactly one generated filename; the file is opened beneath the resolved history root through os.Root so a swapped symlink cannot escape, and must be a regular file.

func (*Store) Rebind

func (s *Store) Rebind(key, absPath string) error

Rebind points an existing history at a new absolute path. Used when a file with a known id turns out to be a rename rather than a clone.

func (*Store) ResolveIdentity

func (s *Store) ResolveIdentity(absPath, diskID string) (id string, provisional bool, err error)

ResolveIdentity resolves the durable identity of a .htmlclay file at first open, in one store transaction and without writing anything to disk. The caller injects the returned id into the bytes it serves; the client's own save is what puts it on disk. This is the whole reason serving no longer mutates a file.

diskID is the id currently in the file's bytes ("" if absent or not a canonical UUID). The rules, in order:

  1. If this exact absPath already owns an id-history, that identity wins over whatever the disk bytes carry. A file clobbered in place by a copy of a different document keeps the resident history instead of forking (model B′): the pre-clobber versions stay restorable.
  2. Otherwise a valid diskID runs the normal owned / renamed / clone claim.
  3. A missing diskID, or a clone, mints a fresh id. A freshly minted id is provisional: its only history is the first-open snapshot, which PruneAll deletes if no save ever makes it durable.

func (*Store) SetProvisional

func (s *Store) SetProvisional(key, absPath string, provisional bool) error

SetProvisional records whether key's history is a first-open snapshot taken under a freshly minted identity that no save has yet made durable. A first-open snapshot sets it true; the first save clears it (both explicitly here, and implicitly because writeMeta defaults it false on the save's own backup).

Jump to

Keyboard shortcuts

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