cs3state

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 21, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package cs3state stores the service's own state inside OpenCloud, over CS3.

This is a deliberate choice with real trade-offs, so they are written down here rather than discovered later:

  • **Why.** The deployment already has one durable, backed-up, operated storage system: OpenCloud's. Adding a second one (a database and its volume) for a family-scale plugin is infrastructure the operator did not ask for. State goes where the data already lives.

  • **What it costs.** A CS3 Space is a filesystem, not a database: no transactions, no compare-and-set. Nothing in this package pretends otherwise. Mutual exclusion between runs is process-local (internal/jobs); the durable lease is read as well — by the next acquire, by the scheduler's busy check, and by the rotation commands — but a read-then-write cannot be made atomic here, so it catches the mistake that happens rather than every possible race, and it is what lets a *crashed* process be cleaned up after. That is sound for the locked single-instance deployment and unsound for a scaled-out one. Running two instances against one state Space is unsupported.

  • **Where.** A dedicated Space, configured by the operator, that no end user is a member of. It must not be a user's Space: everything here is service-internal, a member could delete it, and the backup service's memory is not a user's document. What is stored is metadata plus already wrapped envelopes (SRW/TW ciphertext) — never plaintext key material — but that is a defence in depth, not a licence to put it somewhere readable.

Keys map to paths: "jobs/space/123" becomes "<prefix>/jobs/space/123". Key segments are pre-escaped by internal/state to a safe alphabet, so no key can walk out of the prefix.

Index

Constants

View Source
const DefaultPrefix = ".backup-service-state"

DefaultPrefix is the folder the service keeps its state in.

Variables

View Source
var ErrUnsafeStateSpace = errors.New("cs3state: unsafe state space")

ErrUnsafeStateSpace reports a state Space an end user can reach. It is distinguishable so startup can treat it as a misconfiguration to refuse, rather than as the transient "OpenCloud is not up yet" it would otherwise look like.

Functions

This section is empty.

Types

type Options

type Options struct {
	// SpaceID names the Space that holds service state. Required.
	SpaceID string
	// Prefix is the folder inside that Space; empty uses DefaultPrefix.
	Prefix string
	// ServiceAccountID is the principal the store itself reaches the Space
	// with. Check treats a grant held by exactly this principal as the
	// service's own access rather than as an end user's, because that is what
	// it is: creating a project Space over CS3 leaves the creator a manager,
	// and the creator here is the service. Empty keeps Check strict, so an
	// unconfigured deployment cannot accidentally widen the predicate.
	ServiceAccountID string
}

Options configures a Store.

type SpaceClient

type SpaceClient interface {
	cs3.SpaceReader
	cs3.SpaceWriter
}

SpaceClient is the CS3 surface this store needs.

type Store

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

Store is a state.Store backed by a folder in an OpenCloud Space.

func New

func New(client SpaceClient, opts Options) (*Store, error)

New constructs a Store. The Space is resolved lazily on first use, so a service can start before OpenCloud is reachable.

func (*Store) Check

func (s *Store) Check(ctx context.Context) error

Check resolves the state Space and refuses it if end users can reach it.

A Space someone is a member of is a Space someone can empty. The records in here are the service's memory — including the only server-side copy of every wrapped Data Key — and a user who deletes a folder they do not recognise would be destroying backups without ever being told so. A personal Space is refused for the same reason plus one more: it belongs to a person, and this is not their document.

It is called at startup so the deployment fails loudly rather than discovering the problem the day the folder disappears. The Space's name is reported to make the misconfiguration fixable; its members never are.

The service account's own grant does not count, and that distinction is load-bearing rather than a convenience. OpenCloud 7.3.0 gives every project Space a manager grant at creation and refuses to remove the last one ("cannot remove the last share with manager permissions on a space root"), so a Space with literally zero grants cannot be created at all. The predicate that can be satisfied — and the one decisions.md #16 actually states — is "no *end user* is a member". A grant held by the principal this store authenticates as is the service's access to its own memory; counting it would refuse the only Space that can exist.

func (*Store) Create

func (s *Store) Create(ctx context.Context, key string, value []byte) error

Create stores value under key, refusing to replace an existing document.

Whether the upload itself would clobber is a property of the OpenCloud version underneath (pinned by TestIntegration_CS3StateOverwriteSemantics), so the guard does not rely on it: the parent folder is listed first, and a reva ALREADY_EXISTS is mapped on top of that. The listing is what makes this safe on a server that overwrites silently.

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, key string) error

Delete removes key.

func (*Store) Get

func (s *Store) Get(ctx context.Context, key string) ([]byte, error)

Get returns the value stored under key.

func (*Store) List

func (s *Store) List(ctx context.Context, prefix string) ([]string, error)

List returns the keys under prefix, in lexical order.

func (*Store) Replace

func (s *Store) Replace(ctx context.Context, key string, value []byte) error

Replace stores value under key, discarding any previous value.

The upload path may refuse to clobber (restores must never overwrite), so a replacement can degrade to delete-then-write. That is not atomic: a crash between the two loses the record. Only records the service can re-derive after a restart may be written this way — leases and instance records, which expire, and job records, which the next run rewrites. Everything whose loss is permanent is append-only instead (see state.Versions).

Jump to

Keyboard shortcuts

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