API: backups manager
Several named backups on one destination: a personal one, ones the user created,
ones other people share with them. This is the headless engine;
<selfstore-backups> is its panel.
You need it when a destination holds more than one backup file. A store with a single durable home does not.
import { createBackupsManager } from 'selfstore/backups';
const manager = createBackupsManager({
store: store.advanced,
kv: store.flowHost.kv,
host: myDriveHost,
naming: { canonicalName: 'my-app.zip' }
});
await manager.hydrate();
createBackupsManager
function createBackupsManager(deps: {
store: LocalStore;
kv: KV;
host: BackupsHost;
naming: BackupsNaming;
keys?: BackupsKeys;
}): BackupsManager;
Drive, without implementing anything
driveBackupsHost is a ready-made BackupsHost for Google Drive, so an app
storing there writes none of the contract below:
import { driveBackupsHost } from 'selfstore/backups';
const host = driveBackupsHost({ auth, kv, fileName: 'acme.zip', nameContains: 'acme' });
DriveBackupsHostOptions extends the usual DriveOptions with one field:
nameContains, which narrows the listing server-side. selfstore ships no naming
convention, so without it every file the app can see is a candidate.
Implement the contract below only for a destination selfstore does not ship.
BackupsHost: what you implement
The manager knows nothing about your destination. It asks this contract for everything, so the same panel drives Drive, a WebDAV folder or your own service.
| Member | Signature | Required |
|---|---|---|
kind |
string |
yes |
activeIdKey |
string |
yes - the KV key holding the active file id |
list |
() => Promise<BackupFileInfo[]> |
yes |
open |
(fileId: string) => BackupTarget |
yes |
create |
(fileName: string) => Promise<{ fileId: string }> |
yes |
remove |
(fileId: string) => Promise<void> |
yes |
findOrCreatePersonal |
() => Promise<{ fileId, created } | null> |
yes |
rename |
(fileId, fileName) => Promise<void> |
optional |
ensureSession |
() => Promise<boolean> |
optional |
fileOwner |
(fileId) => Promise<{ email, name } | null> |
optional |
excludedFileIds |
() => Promise<(string | null | undefined)[]> |
optional |
The optional ones are capabilities, not niceties: a host without rename simply
has no rename action, rather than a button that fails.
interface BackupFileInfo {
id: string;
name: string;
modifiedTime: string | null;
}
BackupsNaming
interface BackupsNaming {
canonicalName: string; // the personal backup's file name
namedFileFor?(label: string): string; // label -> file name
parseLabel?(name: string): string | null | undefined; // file name -> label
}
Supply the pair when your users name their backups. Without them every backup uses the canonical name and only one exists.
BackupsKeys
interface BackupsKeys {
registry?: string;
encrypted?: string;
shared?: string;
joined?: string;
owner?: string;
}
The KV keys the manager bookkeeps under. Override them only when two managers share one KV.
BackupsSnapshot
interface BackupsSnapshot {
registry: KnownBackups;
activeFileId: string | null;
joined: boolean;
owner: BackupOwner | null;
lastError: BackupsErrorCode | null;
}
interface KnownBackups {
personalFileId: string | null;
shared: { fileId: string; ownerEmail: string | null; ownerName: string | null }[];
}
type BackupsErrorCode = 'cancelled' | 'gone' | 'failed';
'cancelled' is a user gesture, not a failure - a picker they closed. Treat it
as nothing happened.
BackupRow
What list() returns, and what a panel renders:
interface BackupRow {
fileId: string;
name: string;
label: string | null;
modifiedAt: number | null;
encrypted: boolean | null;
shared: boolean | null;
}
encrypted and shared are three-state. null means not yet known: the
listing is cheap, the encryption probe is not, so a row appears immediately and
learns what it is afterwards. Render null as “unknown”, never as “no”.
Methods
| Method | Signature | Notes |
|---|---|---|
hydrate |
() => Promise<void> |
Load the registry. Call once at boot. |
refresh |
() => Promise<void> |
Re-read from the destination. |
list |
() => Promise<BackupRow[]> |
|
markActive |
(fileId: string) => Promise<void> |
|
probeEncryption |
(fileId: string) => Promise<boolean | null> |
Cheap header read; no decrypt. |
noteShared |
(fileId: string, shared: boolean) => Promise<void> |
Record what your app knows. |
openBackup |
(fileId, passphrase?) => Promise<'ok' | 'encrypted' | 'failed'> |
'encrypted' means ask for a password and call again. |
openPersonal |
(passphrase?) => Promise<'ok' | 'encrypted' | 'failed'> |
|
createNamed |
(label: string) => Promise<'ok' | 'failed'> |
|
createShared |
(fileName: string, owner: BackupOwner) => Promise<'ok' | 'failed'> |
|
registerShared |
(fileId: string, owner: BackupOwner) => Promise<void> |
|
renameBackup |
(fileId: string, label: string) => Promise<'ok' | 'failed'> |
|
deleteBackup |
(fileId: string) => Promise<boolean> |
|
forgetShared |
(fileId: string) => Promise<void> |
Drop it locally; the file stays. |
fileNameFor |
(label: string) => string |
The three-way return of openBackup is the shape worth copying: 'encrypted'
is not an error, it is the journey asking for the next input. A boolean would
force you to inspect an error code to tell “wrong password” from “needs one”.
The manager is a FlowStore<BackupsSnapshot>, so snapshot and subscribe
work exactly as in flows.
Forgetting is not deleting
forgetShared drops a row from this device’s registry. deleteBackup removes
the file. The panel keeps them as separate gestures with separate confirmations,
because one is reversible by re-adding the share and the other is not.