Documentation
¶
Index ¶
- Variables
- func DecryptConfigFiles(srcDir, destDir string, key []byte) error
- func DecryptConfigFilesScoped(files []string, srcDir, destDir string, key []byte) error
- func DeriveKey(password string, salt []byte) []byte
- func EffectiveSyncFiles(scope []string) []string
- func EncryptConfigFiles(srcDir, destDir string, key []byte) error
- func EncryptConfigFilesScoped(files []string, srcDir, destDir string, key []byte) error
- func GenerateSalt() ([]byte, error)
- func ReadSaltFile(repoPath string) ([]byte, error)
- func TestConnection(repoURL, username, token string) error
- func WriteSaltFile(repoPath string, salt []byte) error
- type BranchHeads
- type ConflictInfo
- type GitRepo
- func (g *GitRepo) CommitDepth() (int, error)
- func (g *GitRepo) CommitTime(h plumbing.Hash) (time.Time, error)
- func (g *GitRepo) ExtractCommitFiles(hash plumbing.Hash, destDir string) error
- func (g *GitRepo) ExtractCommitFilesIn(hash plumbing.Hash, files []string, destDir string) error
- func (g *GitRepo) Fetch(username, token string) error
- func (g *GitRepo) HeadRef() (*plumbing.Reference, error)
- func (g *GitRepo) MergeBase(a, b plumbing.Hash) (*plumbing.Hash, error)
- func (g *GitRepo) Pull(username, token string) error
- func (g *GitRepo) Push(username, token string) error
- func (g *GitRepo) PushForce(username, token string) error
- func (g *GitRepo) PushToBranch(branch, username, token string) error
- func (g *GitRepo) ReadRemoteFile(branch, filePath string) ([]byte, error)
- func (g *GitRepo) ResetToRemote(branch string) error
- func (g *GitRepo) ResolveBranchHeads(branch string) (BranchHeads, error)
- func (g *GitRepo) RestoreHead(refName plumbing.ReferenceName, hash plumbing.Hash) error
- func (g *GitRepo) RewriteHistory(keep int) error
- func (g *GitRepo) StageAndCommit(msg string) (bool, error)
- func (g *GitRepo) StageAndCommitFiles(msg string, files []string) (bool, error)
- type Keychain
- func (k *Keychain) Delete(key string) error
- func (k *Keychain) DeleteModelAPIKey(modelID string) error
- func (k *Keychain) DeletePassword(connID string) error
- func (k *Keychain) DeleteRemoteSecret(backend string) error
- func (k *Keychain) Get(key string) (string, error)
- func (k *Keychain) GetEncryptionKey() ([]byte, error)
- func (k *Keychain) GetGitToken() (string, error)
- func (k *Keychain) GetModelAPIKey(modelID string) (string, error)
- func (k *Keychain) GetPassword(connID string) (string, error)
- func (k *Keychain) GetRemoteSecret(backend string) (string, error)
- func (k *Keychain) GetWebDAVPassword() (string, error)
- func (k *Keychain) List() ([]string, error)
- func (k *Keychain) Set(key, value string) error
- func (k *Keychain) SetGitToken(token string) error
- func (k *Keychain) SetModelAPIKey(modelID, apiKey string) error
- func (k *Keychain) SetPassword(connID, password string) error
- func (k *Keychain) SetRemoteSecret(backend, secret string) error
- func (k *Keychain) SetWebDAVPassword(pw string) error
- func (k *Keychain) StoreEncryptionKey(key []byte) error
- type PasswordStore
- type RemoteConfig
- type RemoteFS
- type SecretStore
- type SnapshotBackend
- type SnapshotInfo
- type SyncConfig
- type SyncConfigStore
- type SyncDirection
- type SyncResult
- type SyncService
- func (s *SyncService) ChangePassword(oldPassword, newPassword string) error
- func (s *SyncService) ConfigureRepo(repoURL, username, token, masterPassword string) (*SyncResult, error)
- func (s *SyncService) ConfigureRepoRemote(backend string, remote RemoteConfig, secret, masterPassword string) (*SyncResult, error)
- func (s *SyncService) DeleteRepo() error
- func (s *SyncService) GetConfig() (SyncConfig, error)
- func (s *SyncService) IsAutoSyncEnabled() bool
- func (s *SyncService) PasswordStore() *Keychain
- func (s *SyncService) Ready() <-chan struct{}
- func (s *SyncService) RepoPath() string
- func (s *SyncService) ResolveConflict(useLocal bool) (*SyncResult, error)
- func (s *SyncService) SaveConfig(config SyncConfig, token string) error
- func (s *SyncService) SetPasswordStore(ps PasswordStore)
- func (s *SyncService) SetSecretBackend(backend SecretStore)
- func (s *SyncService) SetStores(stores []SyncableStore)
- func (s *SyncService) Sync() (*SyncResult, error)
- func (s *SyncService) TestConnection() error
- func (s *SyncService) TestRemote(backend string, remote RemoteConfig, secret string) error
- func (s *SyncService) UpdateRemotePassword(secret, masterPassword string) error
- func (s *SyncService) VerifySyncPassword(password, username, token string) error
- type SyncableStore
Constants ¶
This section is empty.
Variables ¶
var ErrRemoteNotFound = errors.New("sync: remote resource not found")
ErrRemoteNotFound reports that a resource is absent on the sync remote. Every RemoteFS implementation normalizes its backend's own not-found error to this sentinel, so the snapshot engine can tell "remote is empty" (Head returning nil) apart from a real failure.
var ErrWrongSyncPassword = errors.New("WRONG_SYNC_PASSWORD")
Functions ¶
func DecryptConfigFiles ¶
DecryptConfigFiles decrypts the whole default sync set. Kept for callers/tests outside a SyncService scope context (the password rotation and the wrong-key guard work on staging dirs, not the store registry).
func DecryptConfigFilesScoped ¶ added in v1.10.0
DecryptConfigFilesScoped decrypts exactly the given config files from srcDir into destDir. files must come from EffectiveSyncFiles. Byte-level: read the encrypted blob, decryptBytes under the sync key, write the plaintext — no field handling. A blob the source never carried is skipped: sync must not stage an empty document that would disagree with the null sentinel hashStoresScoped records for a store whose backing file was never written.
func DeriveKey ¶
DeriveKey derives a 32-byte AES-256 key from a password and salt using PBKDF2-SHA256. DeriveKey derives the sync encryption key. Re-exported from backend/kdf (the shared leaf package credentials also uses), so existing call sites inside and outside the package keep compiling.
func EffectiveSyncFiles ¶ added in v1.10.0
EffectiveSyncFiles resolves the user's scope against syncableFiles. A nil/empty scope returns the default set. Unknown names are dropped so a stale scope list from an older or newer build can never smuggle an unintended file into the repo.
func EncryptConfigFiles ¶
EncryptConfigFiles encrypts the whole default sync set. Kept for callers/tests outside a SyncService scope context (the password rotation re-encrypts a staging dir rather than going through the store registry).
func EncryptConfigFilesScoped ¶ added in v1.10.0
EncryptConfigFilesScoped encrypts exactly the given config files from srcDir into destDir. files must come from EffectiveSyncFiles. The body is byte-level: read the plaintext blob, encryptBytes under the sync key, write — all field-level semantics live in the stores (ExportPlain / ImportPlain), so nothing here knows what a secret field is. A file srcDir never carried is skipped: sync must not upload a document that would clobber the remote copy.
func GenerateSalt ¶
GenerateSalt generates a random 16-byte salt. Re-exported from backend/kdf; see DeriveKey.
func ReadSaltFile ¶
Returns nil if the file doesn't exist (new repo).
func TestConnection ¶
TestConnection verifies the repo URL is reachable.
func WriteSaltFile ¶
WriteSaltFile writes the salt to .sync-salt in the repo directory.
Types ¶
type BranchHeads ¶ added in v1.9.2
BranchHeads is the resolved state of the sync branch after a fetch. A nil Local means HEAD is still unborn (no commits yet); a nil Remote means the remote branch does not exist (empty repository).
type ConflictInfo ¶
type GitRepo ¶
type GitRepo struct {
// contains filtered or unexported fields
}
func CloneOrOpen ¶
CloneOrOpen opens the repo at repoPath, or clones it from the given URL.
func (*GitRepo) CommitDepth ¶ added in v1.10.0
CommitDepth counts commits along first-parent from HEAD. The clone is single-branch, so HEAD is always the sync branch. An unborn HEAD (no commits yet) counts as 0. The result is capped at depthScanCap.
func (*GitRepo) CommitTime ¶ added in v1.9.2
CommitTime returns the committer timestamp of the given commit.
func (*GitRepo) ExtractCommitFiles ¶ added in v1.9.2
ExtractCommitFiles writes the whole default sync set as committed at hash into destDir. The extracted files are still encrypted — the caller decrypts them. Files absent from that commit's tree are skipped.
func (*GitRepo) ExtractCommitFilesIn ¶ added in v1.10.0
ExtractCommitFilesIn writes exactly files as committed at hash into destDir (still encrypted). Files absent from the tree are skipped. files must come from EffectiveSyncFiles.
func (*GitRepo) HeadRef ¶ added in v1.10.0
HeadRef returns the resolved HEAD branch reference (name and hash), so callers can remember the pre-mutation head and restore it if a later step fails.
func (*GitRepo) MergeBase ¶ added in v1.9.2
MergeBase returns the best common ancestor of the two commits, or nil when they share no history.
func (*GitRepo) PushForce ¶ added in v1.10.0
PushForce force-pushes HEAD to its upstream, replacing remote history. Other devices lose their merge base against the new head — Sync handles that explicitly (nil merge base → direct content comparison).
func (*GitRepo) PushToBranch ¶
PushToBranch pushes the current HEAD to the specified remote branch.
func (*GitRepo) ReadRemoteFile ¶
ReadRemoteFile reads a file from the remote tracking branch without touching the worktree.
func (*GitRepo) ResetToRemote ¶
ResetToRemote resets local HEAD to match remote branch.
func (*GitRepo) ResolveBranchHeads ¶ added in v1.9.2
func (g *GitRepo) ResolveBranchHeads(branch string) (BranchHeads, error)
ResolveBranchHeads returns the local HEAD hash and the origin/<branch> hash without assuming either exists.
func (*GitRepo) RestoreHead ¶ added in v1.10.0
RestoreHead repoints the branch reference at hash — the rollback for a history rewrite whose force-push failed.
func (*GitRepo) RewriteHistory ¶ added in v1.10.0
RewriteHistory rebuilds the branch history keeping only the most recent keep commits, cutting everything older. Each rebuilt commit preserves the original tree, author, committer, message and timestamps; the new root has no parent. When the existing depth is within keep, the rebuild reproduces byte-identical commit objects (same hashes) and is a no-op.
Rebuilt hashes differ from the originals once history is actually cut, so other devices lose their merge base against the rewritten chain — Sync must treat a nil merge base explicitly. Only the local branch is repointed; call PushForce to publish the rewritten chain. The remote tracking ref (refs/remotes/origin/...) is NOT updated — it stays stale until the next fetch or a re-clone, which is what the compaction caller does. Merge commits inside the kept window lose their second parent, so even a depth ≤ keep rebuild is only byte-identical for linear history.
func (*GitRepo) StageAndCommit ¶
StageAndCommit stages the whole default sync set plus repo metadata. Returns true if committed.
func (*GitRepo) StageAndCommitFiles ¶ added in v1.10.0
StageAndCommitFiles stages exactly files plus ".sync-salt"/"README.md". Whitelisting keeps stray files (e.g. an SSH key) out of the repo (SYNC-P1-9). files must come from EffectiveSyncFiles; arbitrary lists weaken the SYNC-P1-9 stray-file whitelist.
type Keychain ¶
type Keychain struct {
// contains filtered or unexported fields
}
func NewKeychain ¶
func NewKeychain() *Keychain
func (*Keychain) DeleteModelAPIKey ¶
func (*Keychain) DeletePassword ¶
func (*Keychain) DeleteRemoteSecret ¶ added in v1.10.1
DeleteRemoteSecret removes the credential of a snapshot backend.
func (*Keychain) GetEncryptionKey ¶
GetEncryptionKey retrieves the derived encryption key from OS keychain.
func (*Keychain) GetGitToken ¶
func (*Keychain) GetRemoteSecret ¶ added in v1.10.1
GetRemoteSecret reads the password-class credential of a snapshot backend. A missing entry reads as empty, like the WebDAV helper.
func (*Keychain) GetWebDAVPassword ¶ added in v1.10.0
func (*Keychain) List ¶ added in v1.10.1
List returns the entry names stored under the app's keychain service. Used by credential recovery to find keys written under another data-dir hash (copied/moved data directory). Platforms without enumeration return an error — recovery is simply skipped there.
func (*Keychain) SetGitToken ¶
func (*Keychain) SetModelAPIKey ¶
func (*Keychain) SetPassword ¶
func (*Keychain) SetRemoteSecret ¶ added in v1.10.1
SetRemoteSecret stores (or clears, on empty) the credential of a snapshot backend.
func (*Keychain) SetWebDAVPassword ¶ added in v1.10.0
func (*Keychain) StoreEncryptionKey ¶
StoreEncryptionKey stores the derived encryption key in OS keychain.
type PasswordStore ¶ added in v1.8.0
type PasswordStore interface {
// Unlocked reports whether the credential store currently holds a usable
// key. When false (master-password not yet entered, keychain lost, or never
// set up), sync must refuse to run rather than leak or mis-carry secrets.
Unlocked() bool
}
PasswordStore is the local credential store as sync sees it. Sync only consults Unlocked: every field-level concern (which fields are secret, enc:v1: encryption, legacy keychain backfill) lives in the stores, so a locked vault is the only thing that can still make sync misbehave.
type RemoteConfig ¶ added in v1.10.1
type RemoteConfig struct {
// Server is the WebDAV server URL; for sftp/smb host[:port]; for s3 the
// endpoint (required — custom endpoints cover MinIO/OSS/COS, and AWS is
// reachable through s3.amazonaws.com).
Server string `json:"server,omitempty"`
// Path is the remote directory (webdav/sftp/smb) or object key prefix
// (s3) holding the snapshot.
Path string `json:"path,omitempty"`
// User authenticates the share; for s3 it is the access key (the secret
// key rides in the keychain, matching the session-layer field semantics).
User string `json:"user,omitempty"`
// Bucket is s3-only.
Bucket string `json:"bucket,omitempty"`
// Region is s3-only (signature scope).
Region string `json:"region,omitempty"`
// URLStyle is s3-only: ""/"virtual" uses virtual-hosted URLs — required
// by OSS/COS/OBS which reject path-style requests; "path" keeps
// endpoint/bucket/key addressing for AWS S3 and MinIO.
URLStyle string `json:"urlStyle,omitempty"`
// Domain is smb-only (NTLM domain; empty = local account).
Domain string `json:"domain,omitempty"`
Share string `json:"share,omitempty"`
}
RemoteConfig is the connection description shared by every snapshot backend (webdav/sftp/s3/smb). The pattern mirrors the original WebDAV config: server + remote path + user, with the password-class secret kept out of the JSON in the keychain (Keychain.GetRemoteSecret) — only the non-secret fields live here.
type RemoteFS ¶ added in v1.10.1
type RemoteFS interface {
// Probe verifies reachability and credentials. Idempotent.
Probe() error
// Get reads a file and returns ErrRemoteNotFound when it is absent.
Get(name string) ([]byte, error)
// Put writes a file, creating any missing parent directories.
Put(name string, data []byte) error
}
RemoteFS is the minimal filesystem view a snapshot backend needs on the remote: verify reachability, read a file, write a file. The snapshot engine (remote_snapshot.go) turns this into the SnapshotBackend contract of Probe, Head, FetchAll and StoreAll, so one engine serves all remotes.
name arguments are bare file names such as manifest.json or connections.json; each implementation owns the base path and joins it.
type SecretStore ¶ added in v1.12.3
type SecretStore interface {
Get(key string) (string, error)
Set(key, value string) error
Delete(key string) error
List() ([]string, error)
}
SecretStore replaces the platform keychain as the backing storage of a Keychain. Headless server containers have no OS keychain (no Secret Service D-Bus), so the server build wires an encrypted-file store here; desktop builds leave it nil and use the platform keychain functions.
type SnapshotBackend ¶ added in v1.10.0
type SnapshotBackend interface {
// Probe verifies reachability/credentials and creates the remote
// collection if missing.
Probe() error
// Head returns the current remote snapshot, or nil when the remote
// is empty (freshly created / wiped).
Head() (*SnapshotInfo, error)
// FetchAll downloads the current snapshot's encrypted resources into
// dir (.sync-salt + ciphertext files + manifest.json). A missing
// remote snapshot leaves dir untouched.
FetchAll(dir string) error
// StoreAll uploads the encrypted files from stagingDir as a new
// snapshot with the given salt and manifest. The manifest is written
// last so readers never observe a half-written snapshot.
StoreAll(stagingDir string, salt []byte, manifest SnapshotInfo) error
}
SnapshotBackend is the contract for snapshot-type sync remotes.
func NewRemoteSnapshotBackend ¶ added in v1.10.1
func NewRemoteSnapshotBackend(fs RemoteFS, files []string) SnapshotBackend
NewRemoteSnapshotBackend builds the engine over fs. files is the scoped file list used by StoreAll's staging upload.
func NewWebDAVBackend ¶ added in v1.10.0
func NewWebDAVBackend(serverURL, basePath, user, password string, files []string) SnapshotBackend
NewWebDAVBackend builds the engine over a WebDAV share through the session layer, dialing a dedicated session for the sync run. Kept as the legacy constructor name so existing call sites and tests stay valid.
type SnapshotInfo ¶ added in v1.10.0
type SnapshotInfo struct {
// Version is an opaque token that changes on every successful upload
// (timestamp-based; no server-side monotonic counter is assumed). It
// must be unique per upload — the sync driver re-Heads after StoreAll
// to detect lost-update races, and a duplicate Version makes that
// check meaningless. Writers should use an RFC3339Nano timestamp plus
// a random suffix.
Version string `json:"version"`
UpdatedAt time.Time `json:"updatedAt"`
// FileHashes maps config file name → hash of its canonical plaintext
// (hashStoresScoped / canonicalPlainHash), so hashes are
// device-independent.
FileHashes map[string]string `json:"fileHashes"`
}
SnapshotInfo describes one remote snapshot state. In git mode history provides the merge base; snapshot backends (WebDAV, future S3/OneDrive) carry FileHashes instead and the three-way base lives in the device's SyncConfig.LastSyncedHash.
type SyncConfig ¶
type SyncConfig struct {
RepoURL string `json:"repoUrl"`
Branch string `json:"branch"`
Username string `json:"username"`
AutoSync bool `json:"autoSync"`
// SyncScope is this device's per-file sync selection over syncableFiles.
// nil/empty resolves to the default set (defaultSyncedFiles).
SyncScope []string `json:"syncScope,omitempty"`
// Backend selects the sync transport: "git" (default, private git repo)
// or one of the snapshot backends "webdav"/"sftp"/"s3"/"smb".
Backend string `json:"backend,omitempty"`
Remote RemoteConfig `json:"remote,omitempty"`
// Legacy flat WebDAV fields — read by Load for configs written before
// RemoteConfig existed, written back only as a transition dual-write in
// ConfigureRepoRemote. New code must use Remote.
WebDAVServer string `json:"webdavServer,omitempty"`
WebDAVPath string `json:"webdavPath,omitempty"`
WebDAVUser string `json:"webdavUser,omitempty"`
// LastSyncedHash is the aggregate content hash both sides agreed on at
// the last successful snapshot sync (three-way base for snapshot mode).
LastSyncedHash string `json:"lastSyncedHash,omitempty"`
LastSyncAt time.Time `json:"lastSyncAt"`
LastSyncStatus string `json:"lastSyncStatus"`
LastSyncError string `json:"lastSyncError"`
}
type SyncConfigStore ¶
type SyncConfigStore struct {
// contains filtered or unexported fields
}
func NewSyncConfigStore ¶
func NewSyncConfigStore(configDir string) *SyncConfigStore
func (*SyncConfigStore) Load ¶
func (s *SyncConfigStore) Load() (SyncConfig, error)
func (*SyncConfigStore) Save ¶
func (s *SyncConfigStore) Save(config SyncConfig) error
type SyncDirection ¶
type SyncDirection int
const ( SyncNone SyncDirection = iota SyncPush SyncPull SyncConflict )
type SyncResult ¶
type SyncResult struct {
Direction SyncDirection `json:"direction"`
Message string `json:"message"`
Conflict *ConflictInfo `json:"conflict,omitempty"`
}
type SyncService ¶
type SyncService struct {
// contains filtered or unexported fields
}
func NewSyncService ¶
func NewSyncService(dataDir string) (*SyncService, error)
func NewSyncServiceAsync ¶ added in v1.7.0
func NewSyncServiceAsync(dataDir string) (*SyncService, context.Context)
NewSyncServiceAsync returns a SyncService whose disk-touching init (UserConfigDir / MkdirAll / NewKeychain) runs on a background goroutine. F-407: macOS Security framework keychain IPC + the PBKDF2 600k iterations probe can take 50–500ms+ on first launch — doing it synchronously inside wails.OnStartup blocks the first paint of the main window.
Callers should `Ready()` (with a short timeout) before invoking service methods; otherwise methods may fail with ErrSyncNotInitialized until init completes.
func (*SyncService) ChangePassword ¶
func (s *SyncService) ChangePassword(oldPassword, newPassword string) error
ChangePassword re-encrypts every ciphertext file present in the repo with a new master password and a fresh random salt. The rotation is repo-global because the salt is repo-global: every device derives its key from the same salt, so no ciphertext may survive under the old key. A new salt is required so PBKDF2 work cannot be amortized across old and new passwords (SYNC-P1-6). The repo's existing ciphertext is decrypted with the old key and re-encrypted with the new key via a temp-then-rename pattern, so a crash mid-rotation leaves the repo decryptable with the old key.
func (*SyncService) ConfigureRepo ¶
func (s *SyncService) ConfigureRepo(repoURL, username, token, masterPassword string) (*SyncResult, error)
ConfigureRepo sets up a new or existing sync repository.
func (*SyncService) ConfigureRepoRemote ¶ added in v1.10.1
func (s *SyncService) ConfigureRepoRemote(backend string, remote RemoteConfig, secret, masterPassword string) (*SyncResult, error)
ConfigureRepoRemote sets up (or adopts) a snapshot remote for any of the snapshot backends (webdav/sftp/s3/smb). An existing remote snapshot is adopted: its salt is the repo identity, the master password is verified against its ciphertext, and the local side either pulls (no local data), records the agreed hash (identical), or surfaces the conflict dialog (both sides have data). A fresh remote generates a new salt and publishes the local config as the initial snapshot.
func (*SyncService) DeleteRepo ¶
func (s *SyncService) DeleteRepo() error
DeleteRepo removes the local sync repo and credentials.
func (*SyncService) GetConfig ¶
func (s *SyncService) GetConfig() (SyncConfig, error)
GetConfig returns the current sync configuration.
func (*SyncService) IsAutoSyncEnabled ¶
func (s *SyncService) IsAutoSyncEnabled() bool
IsAutoSyncEnabled returns whether auto sync is enabled and configured for the active backend (git repo URL, or a WebDAV server in webdav mode).
func (*SyncService) PasswordStore ¶
func (s *SyncService) PasswordStore() *Keychain
PasswordStore returns the keychain as a PasswordStore for connection store integration.
func (*SyncService) Ready ¶ added in v1.7.0
func (s *SyncService) Ready() <-chan struct{}
Ready returns a channel closed once init has finished. Pair with a short timeout in callers that race startup (F-407).
func (*SyncService) RepoPath ¶
func (s *SyncService) RepoPath() string
RepoPath returns the local git repo path.
func (*SyncService) ResolveConflict ¶
func (s *SyncService) ResolveConflict(useLocal bool) (*SyncResult, error)
ResolveConflict handles a conflict by forcing push or reset.
func (*SyncService) SaveConfig ¶
func (s *SyncService) SaveConfig(config SyncConfig, token string) error
SaveConfig persists sync configuration and stores the token if provided.
func (*SyncService) SetPasswordStore ¶ added in v1.8.0
func (s *SyncService) SetPasswordStore(ps PasswordStore)
SetPasswordStore wires the credential store so sync can refuse to run on a locked vault. Field-level encryption is the stores' job now.
func (*SyncService) SetSecretBackend ¶ added in v1.12.3
func (s *SyncService) SetSecretBackend(backend SecretStore)
SetSecretBackend swaps the keychain's backing storage. Server builds wire an encrypted-file store here: the container has no OS keychain (no Secret Service D-Bus), so every platform-keychain write would fail.
func (*SyncService) SetStores ¶ added in v1.10.1
func (s *SyncService) SetStores(stores []SyncableStore)
SetStores wires the config-exchange registry (SyncableStore). Assembled in app.go — the only place that imports both sync and store. nil until set; sync then skips config exchange instead of touching files itself.
func (*SyncService) Sync ¶
func (s *SyncService) Sync() (*SyncResult, error)
Sync runs a full sync cycle: clone/open → fetch → three-way content analysis → commit/pull/push → decrypt.
The remote state is fetched BEFORE anything is committed locally: the old commit-then-fetch order turned "not pulled yet" into a local commit on a stale head, so two machines used alternately diverged and hit the conflict dialog on every open. Content is then analyzed three-way (local data dir vs merge base vs remote head) so that "merely out of date" is a silent pull and only a genuine both-sides edit surfaces the conflict dialog.
func (*SyncService) TestConnection ¶
func (s *SyncService) TestConnection() error
TestConnection verifies the sync backend is reachable with stored credentials.
func (*SyncService) TestRemote ¶ added in v1.10.1
func (s *SyncService) TestRemote(backend string, remote RemoteConfig, secret string) error
TestRemote probes a snapshot remote with form-provided values, without touching the stored configuration — the add-repo dialog's test button. Unlike TestConnection it never reads sync-config.json, so it can verify a candidate remote before the user commits to it.
func (*SyncService) UpdateRemotePassword ¶ added in v1.10.1
func (s *SyncService) UpdateRemotePassword(secret, masterPassword string) error
UpdateRemotePassword swaps the snapshot backend credential without touching any other configuration. Mirrors the git credential-edit semantics: an empty secret means "keep the stored one" — it is verified against the server but never rewritten. A non-empty secret is probed against the server first, and when the remote already holds a snapshot the master password must decrypt it — a typo can neither lock the device out of its own snapshot nor silently save a broken credential. Changing server/path/user is intentionally not possible here: that is a different sync target, which must go through delete + re-configure.
func (*SyncService) VerifySyncPassword ¶
func (s *SyncService) VerifySyncPassword(password, username, token string) error
VerifySyncPassword validates credentials against the remote and verifies the password can decrypt the remote config. username and token are the new values from the form; token may be empty to keep the stored one.
type SyncableStore ¶ added in v1.10.1
type SyncableStore interface {
BlobName() string
ExportPlain() ([]byte, error)
ImportPlain([]byte) error
}
SyncableStore is the only surface the sync package sees for config exchange. Sync never imports store model types: it holds a registry of these interface values, moving canonical plaintext bytes in and out.
The interface lives here, on the consumer side: store's implementations satisfy it structurally, and the registry is assembled in app.go (the only place that imports both packages). backend/store must not import backend/sync — store → credentials → sync means sync → store would cycle, which is also what keeps the dependency graph acyclic.
The contract:
- ExportPlain renders the store's canonical plaintext — the same shape the on-disk JSON carries after decryption (Load applies the store's full semantics: legacy keychain backfill with move semantics, lazy re-encryption, defaults backfill). Secret fields come out as plaintext; the sync layer re-encrypts the whole blob with the sync key downstream, so only the sync key protects it at rest in the repo. A cleared secret exports as cleared — the store is the single source of truth, and a stale keychain entry can never resurrect it through sync.
- A store whose backing file was never written returns an error wrapping fs.ErrNotExist: sync skips such blobs instead of uploading an empty document that would clobber the remote copy.
- ImportPlain persists the blob through the store's Save, so every save-time invariant (password clearing for identity-auth, manifest encryption, hash-skip, quarantine) applies unchanged.