versioning

package
v0.45.1 Latest Latest
Warning

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

Go to latest
Published: Sep 25, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package versioning persists historical snapshots of node contents inside the same storage backend, under a `.versions/<node_id>/<version_n>` prefix.

Snapshots are taken before a destructive write. Every surface that can replace an existing file's bytes calls writehook.BeforeOverwrite first, which lands in GuardOverwrite (guard.go) and snapshots whatever is about to be lost; a snapshot that cannot be taken refuses the write rather than proceeding. save-text snapshots directly through Snapshot instead, because it already holds the node row.

⚠ This paragraph used to claim the same guarantee while it was not true: until the pre-write guard landed, Snapshot was reached from save-text and the versions endpoints and NOWHERE ELSE -- no upload, move, copy, archive extract or AI write path ever called it, so uploading a file over an existing one destroyed the old bytes with no version kept. Do not restate the guarantee here without checking that the call sites still back it.

Listing + restore are exposed via dedicated /api/files/versions endpoints.

Storage drivers that implement storage.Copier get fast server-side copies; anything else falls back to stream-and-rewrite via Read + Write.

Index

Constants

View Source
const DefaultRetention = 20

DefaultRetention is the per-node version count kept by Cleanup if the caller does not specify a different value.

View Source
const SettingKeyKeepN = "versions.keep_n"

SettingKeyKeepN is the settings-table row holding the per-node version keep count. 0 = retention job disabled (unlimited).

View Source
const VersionsPrefix = syspath.Versions

VersionsPrefix is prepended to each snapshot key. Changing it invalidates existing snapshots; do not change without a migration.

Variables

This section is empty.

Functions

This section is empty.

Types

type RetentionResult

type RetentionResult struct {
	KeepN   int `json:"keep_n"`
	Nodes   int `json:"nodes"`
	Deleted int `json:"deleted"`
	Failed  int `json:"failed"`
}

RetentionResult summarises one retention run.

type Service

type Service struct {
	Store    db.Store
	Resolver StorageResolver
	// Body resolves where the LIVE file's bytes are before a snapshot: the
	// driver, or filex's staging area while a staged upload is still
	// transferring. Nil-safe. Without it, snapshotting a file whose overwrite
	// is still in flight would capture the version it is replacing.
	Body *filebody.Resolver
}

Service is the high-level entry point for versioning operations.

func New

func New(store db.Store, resolver StorageResolver) *Service

New constructs a Service.

func (*Service) AttachBody

func (s *Service) AttachBody(b *filebody.Resolver)

AttachBody wires the byte-source resolver (staging-aware snapshots).

func (*Service) Cleanup

func (s *Service) Cleanup(ctx context.Context, nodeID int64, keepN int) (int, error)

Cleanup deletes versions for a node beyond the keepN newest. It also tries to delete the underlying storage objects but tolerates per-object failures.

func (*Service) GuardOverwrite

func (s *Service) GuardOverwrite(ctx context.Context, storageID int64, rel string) error

GuardOverwrite is the versioning half of writehook.BeforeOverwrite: if a catalogued FILE already lives at rel, snapshot it before the caller replaces it. Returns nil when there is nothing to lose (new file, directory, internal path) so a first write costs one indexed lookup and nothing else.

An error here MUST abort the write — see Snapshot's contract: "losing version history is preferable to corruption".

The bytes are copied verbatim, so a client-side (E2E) encrypted file is snapshotted as the ciphertext it already is: restorable by whoever holds the password, unreadable to the server, and with its `filexe2e` magic prefix intact. The guard never reads content and never touches the folder marker (`.filex-e2e.json`) except to snapshot it like any other file — which is a gain, since a lost marker makes an encrypted folder permanently unopenable.

func (*Service) HardDeleteVersion

func (s *Service) HardDeleteVersion(ctx context.Context, versionID int64) error

HardDeleteVersion drops one version row + its storage object (admin op).

func (*Service) KeepN

func (s *Service) KeepN(ctx context.Context) int

KeepN reads the configured per-node keep count. Missing, empty or non-numeric values (and anything < 0) resolve to 0 = disabled.

func (*Service) List

func (s *Service) List(ctx context.Context, nodeID int64) ([]*model.NodeVersion, error)

List returns all versions for one node, newest-first.

func (*Service) Restore

func (s *Service) Restore(ctx context.Context, nodeID, versionID int64, snapshotCurrent bool) error

Restore copies a recorded version back over the live path.

If snapshotCurrent is true the current live content is first snapshotted so the operation is reversible.

func (*Service) RunRetentionLoop

func (s *Service) RunRetentionLoop(ctx context.Context, interval time.Duration)

RunRetentionLoop ticks RunRetentionOnce every interval until ctx is cancelled. First tick happens after `interval`, not immediately (trash.RunDailyLoop pattern).

func (*Service) RunRetentionOnce

func (s *Service) RunRetentionOnce(ctx context.Context) (RetentionResult, error)

RunRetentionOnce applies Cleanup(keepN) to every node that has version rows. A keep_n of 0 is a no-op (disabled). Per-node failures are counted + logged, never fatal — one broken node must not stall the whole sweep.

func (*Service) Snapshot

func (s *Service) Snapshot(ctx context.Context, nodeID int64) (*model.NodeVersion, error)

Snapshot copies the current live storage_key into the .versions/ tree and records a node_versions row. Returns nil with no error when there's nothing to snapshot (node has no live storage_key yet — first ever write).

Callers should invoke this BEFORE a destructive write. If the snapshot itself fails (storage / DB error) the caller should NOT proceed with the destructive write — losing version history is preferable to corruption.

type StorageResolver

type StorageResolver func(int64) (storage.Driver, error)

StorageResolver maps a storage_id to a live driver. Same shape as the resolver used by the rest of the API layer.

Jump to

Keyboard shortcuts

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