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
- Variables
- func Hash(data []byte) string
- func IDFromKey(key string) (string, bool)
- func IsCanonicalUUID(id string) bool
- func Key(absPath string, data []byte) string
- func ParseEntryName(name string) (time.Time, int, error)
- func SyncDir(dir string) error
- type ClaimStatus
- type Entry
- type Store
- func (s *Store) Backup(key, absPath string, content []byte) (bool, error)
- func (s *Store) BaseDir() string
- func (s *Store) BoundPath(key string) (string, bool)
- func (s *Store) Claim(key, absPath string) (ClaimStatus, string, error)
- func (s *Store) Contains(absPath string) bool
- func (s *Store) Dir() (string, error)
- func (s *Store) HasHistory(key, absPath string) bool
- func (s *Store) List(key, absPath string) ([]Entry, error)
- func (s *Store) MaybePrune(key, absPath string)
- func (s *Store) PruneAll()
- func (s *Store) Read(key, absPath, name string) ([]byte, error)
- func (s *Store) Rebind(key, absPath string) error
- func (s *Store) ResolveIdentity(absPath, diskID string) (id string, provisional bool, err error)
- func (s *Store) SetProvisional(key, absPath string, provisional bool) error
Constants ¶
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 ¶
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 IDFromKey ¶
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 ¶
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 ¶
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 ¶
ParseEntryName validates a version filename and returns its ordering key. It accepts exactly one generated filename and nothing else.
func SyncDir ¶
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 (*Store) Backup ¶
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) BoundPath ¶
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 ¶
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 ¶
Dir returns the versions directory, creating it if needed. Used by the tray item so "Backups" opens something that exists.
func (*Store) HasHistory ¶
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) MaybePrune ¶
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 ¶
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 ¶
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 ¶
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:
- 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.
- Otherwise a valid diskID runs the normal owned / renamed / clone claim.
- 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 ¶
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).