Documentation
¶
Index ¶
- Variables
- func HostFromURL(rawURL string) (string, error)
- func IsInsecureScheme(cfg RemoteConfig) bool
- func LsRemote(ctx context.Context, rfs RemoteFS) ([]*core.Reference, error)
- func Register(name string, f ProtocolFactory)
- func SaveCredentials(cf *CredentialsFile) error
- func SaveRemotes(driftDir string, rf *RemotesFile) error
- type Credential
- type CredentialsFile
- type ProtocolFactory
- type RemoteConfig
- type RemoteFS
- type RemoteInfo
- type RemotesFile
- type SMBFS
- func (s *SMBFS) Close() error
- func (s *SMBFS) List(ctx context.Context, p string) ([]RemoteInfo, error)
- func (s *SMBFS) MkdirAll(ctx context.Context, p string) error
- func (s *SMBFS) Read(ctx context.Context, p string) (io.ReadCloser, error)
- func (s *SMBFS) Remove(ctx context.Context, p string) error
- func (s *SMBFS) Stat(ctx context.Context, p string) (*RemoteInfo, error)
- func (s *SMBFS) Write(ctx context.Context, p string, r io.Reader) error
- type SyncOptions
- type SyncStats
- func Pull(ctx context.Context, store storage.Storer, rfs RemoteFS, branch string, ...) (*SyncStats, error)
- func PullDryRun(ctx context.Context, store storage.Storer, rfs RemoteFS, branch string, ...) (*SyncStats, error)
- func Push(ctx context.Context, store storage.Storer, rfs RemoteFS, branch string, ...) (*SyncStats, error)
- func PushDryRun(ctx context.Context, store storage.Storer, rfs RemoteFS, branch string, ...) (*SyncStats, error)
- type WebDAVFS
- func (w *WebDAVFS) Close() error
- func (w *WebDAVFS) List(ctx context.Context, p string) ([]RemoteInfo, error)
- func (w *WebDAVFS) MkdirAll(ctx context.Context, p string) error
- func (w *WebDAVFS) Read(ctx context.Context, p string) (io.ReadCloser, error)
- func (w *WebDAVFS) Remove(ctx context.Context, p string) error
- func (w *WebDAVFS) Stat(ctx context.Context, p string) (*RemoteInfo, error)
- func (w *WebDAVFS) Write(ctx context.Context, p string, r io.Reader) error
Constants ¶
This section is empty.
Variables ¶
var ( // ErrUnsupported is returned by NewRemoteFS when cfg.Type does not match // any registered protocol. ErrUnsupported = os.ErrInvalid // ErrNotFound mirrors storage.ErrNotFound for remote paths. ErrNotFound = os.ErrNotExist )
Sentinel errors returned by the remote package. Use errors.Is to classify.
var ErrInsecureScheme = errors.New("insecure scheme: http requires explicit opt-in via DRIFT_ALLOW_INSECURE=1")
ErrInsecureScheme is returned by NewWebDAVFS when the URL uses http:// and AllowInsecure is false. The porcelain layer sets AllowInsecure based on DRIFT_ALLOW_INSECURE=1; the factory itself never reads env vars so the security decision is explicit in the config.
var ErrNetwork = errors.New("network error")
ErrNetwork indicates a remote I/O failure (connection refused, timeout, DNS failure, etc.). The cmd layer maps it to ExitNetwork so scripts can distinguish network failures from other errors.
Functions ¶
func HostFromURL ¶
HostFromURL extracts the host portion from a remote URL. Works for both webdav (https://host[:port]/path) and smb (smb://host[:port]/share) URLs.
func IsInsecureScheme ¶
func IsInsecureScheme(cfg RemoteConfig) bool
IsInsecureScheme reports whether the configured remote URL uses an unencrypted scheme (http) over which credentials would be sent in cleartext. The porcelain layer calls this after resolving the config to surface a user-facing warning; the remote layer itself never writes to stderr. smb:// and https:// are considered secure.
func Register ¶
func Register(name string, f ProtocolFactory)
Register adds a protocol factory under the given name. It is called from each protocol implementation's init() block. Register panics if a factory is already registered for name, which indicates a duplicate init.
func SaveCredentials ¶
func SaveCredentials(cf *CredentialsFile) error
SaveCredentials writes the user-level credentials.json with 0600 perms. The write is atomic (temp file + rename).
func SaveRemotes ¶
func SaveRemotes(driftDir string, rf *RemotesFile) error
SaveRemotes writes the remotes.json file to the given .drift directory. The write is atomic (temp file + rename).
Types ¶
type Credential ¶
type Credential struct {
Remote string `json:"remote"`
Host string `json:"host"`
User string `json:"user"`
Password string `json:"password"`
}
Credential is a remote-name → password entry. Using the remote name as the key (rather than host+user) avoids collisions when the same host+user is used with different passwords for different remotes (e.g. work vs personal projects on the same NAS).
Password is stored in plaintext in credentials.json (protected only by 0600 file permissions). See credentialFilePerm for the keyring roadmap.
type CredentialsFile ¶
type CredentialsFile struct {
Version int `json:"version,omitempty"`
Credentials []Credential `json:"credentials"`
}
CredentialsFile is the on-disk format of credentials.json. The Version field supports future format migrations; the current on-disk version is 1. A missing version (zero value) indicates a legacy file written before the field was introduced and is read as-is.
func LoadCredentials ¶
func LoadCredentials() (*CredentialsFile, error)
LoadCredentials reads the user-level credentials.json. Returns an empty CredentialsFile (not an error) when the file does not exist.
func (*CredentialsFile) AddOrUpdateCredential ¶
func (cf *CredentialsFile) AddOrUpdateCredential(c Credential) bool
AddOrUpdateCredential adds c to the file, or replaces the existing entry with the same remote name. The returned bool is true when an existing entry was replaced.
func (*CredentialsFile) FindCredential ¶
func (cf *CredentialsFile) FindCredential(remoteName string) (string, error)
FindCredential returns the password for the given remote name, or an error wrapping os.ErrNotExist when no matching credential is configured.
type ProtocolFactory ¶
type ProtocolFactory func(cfg RemoteConfig) (RemoteFS, error)
ProtocolFactory constructs a RemoteFS from a RemoteConfig. Each protocol implementation registers its factory in an init() block via Register.
type RemoteConfig ¶
type RemoteConfig struct {
Name string `json:"name"`
Type string `json:"type"` // "webdav" | "smb" | future
URL string `json:"url"` // webdav: https://host[:port]/path; smb: smb://host[:port]/share[/path]
User string `json:"user"`
Options map[string]string `json:"options,omitempty"` // protocol-specific fields
// AllowInsecure permits http:// URLs. It is a runtime-only flag
// (never persisted to remotes.json) set by the porcelain layer based
// on DRIFT_ALLOW_INSECURE=1. Protocol factories (e.g. NewWebDAVFS)
// check this field and refuse cleartext http unless it is true. This
// centralizes the security check in the constructor so new callers
// cannot bypass it by forgetting to call IsInsecureScheme.
AllowInsecure bool `json:"-"`
}
RemoteConfig describes a single configured remote. Password is NOT stored here — it lives in the user-level credentials.json, matched by remote name. Protocol-specific fields (SMB domain, S3 region/bucket, SFTP key path, etc.) go in Options so adding a new protocol never changes this struct.
The "_password" key in Options is a runtime-only convention: the caller (resolveRemoteConfig) injects the password from credentials.json into Options["_password"] before passing the config to a ProtocolFactory. It must never be persisted to remotes.json or included in log output.
type RemoteFS ¶
type RemoteFS interface {
// Stat returns metadata for a remote path, or an error wrapping
// os.ErrNotExist when the path does not exist.
Stat(ctx context.Context, path string) (*RemoteInfo, error)
// Read opens a remote file for reading. The caller must close the
// returned reader. Returns an error wrapping os.ErrNotExist when the
// path does not exist.
Read(ctx context.Context, path string) (io.ReadCloser, error)
// Write uploads a file. Path's parent directories are created if
// needed. If the file already exists it is overwritten.
Write(ctx context.Context, path string, r io.Reader) error
// Remove deletes a remote file. A missing file is not an error.
Remove(ctx context.Context, path string) error
// List enumerates entries under a directory path. Returns an empty
// slice (not nil) when the directory is empty or does not exist.
List(ctx context.Context, path string) ([]RemoteInfo, error)
// MkdirAll creates a directory tree, similar to os.MkdirAll.
MkdirAll(ctx context.Context, path string) error
// Close releases protocol-level resources (connections, sessions).
// It must be called exactly once when the RemoteFS is no longer needed.
Close() error
}
RemoteFS is the abstract filesystem interface that all remote protocols implement. push/pull logic operates solely against this interface, so adding a new protocol (e.g. S3) only requires implementing it here and registering it in an init() block.
Path contract: all paths passed to these methods are relative to the remote root, use forward slashes (/), and have NO leading slash. The root directory is represented by "" or ".". Path helpers (chunkRemotePath, snapshotRemotePath, etc.) produce paths that already conform to this contract. Each implementation's resolve() method is responsible for converting these relative paths to whatever form the underlying protocol library requires (e.g. gowebdav needs absolute paths with a leading /, go-smb2 needs relative paths without one).
Every method takes a context.Context. The underlying protocol libraries (gowebdav, go-smb2) do not yet honor context for in-flight network calls, so implementations check ctx.Err() at entry and return context.Canceled / context.DeadlineExceeded before issuing the request. This lets callers bail out of long batch operations (push/pull loops) without launching new requests, and leaves the interface ready for context-aware libraries.
func NewRemoteFS ¶
func NewRemoteFS(cfg RemoteConfig) (RemoteFS, error)
NewRemoteFS looks up the registered factory for cfg.Type and constructs a RemoteFS. Returns ErrUnsupported (os.ErrInvalid) for unknown protocol names.
func NewSMBFS ¶
func NewSMBFS(cfg RemoteConfig) (RemoteFS, error)
NewSMBFS constructs an SMBFS from a RemoteConfig. The URL format is:
smb://host[:port]/share[/path]
The password is read from cfg.Options["_password"] (populated by resolveRemoteConfig from the user-level credentials.json). cfg.Options["domain"] optionally specifies the SMB domain.
func NewWebDAVFS ¶
func NewWebDAVFS(cfg RemoteConfig) (RemoteFS, error)
NewWebDAVFS constructs a WebDAVFS from a RemoteConfig. The URL must be a full HTTP/HTTPS WebDAV endpoint (e.g. https://nas.example.com/dav/drift). The password is read from cfg.Options["_password"] (populated by resolveRemoteConfig from the user-level credentials.json).
An http:// URL is refused unless cfg.AllowInsecure is true. The porcelain layer sets AllowInsecure based on DRIFT_ALLOW_INSECURE=1; this factory is the enforcement point so new callers cannot bypass the check by forgetting to call IsInsecureScheme.
type RemoteInfo ¶
RemoteInfo is the metadata returned by Stat and List.
type RemotesFile ¶
type RemotesFile struct {
Remotes []RemoteConfig `json:"remotes"`
}
RemotesFile is the on-disk format of .drift/remotes.json.
func LoadRemotes ¶
func LoadRemotes(driftDir string) (*RemotesFile, error)
LoadRemotes reads the remotes.json file from the given .drift directory. Returns an empty RemotesFile (not an error) when the file does not exist.
func (*RemotesFile) AddOrUpdateRemote ¶
func (rf *RemotesFile) AddOrUpdateRemote(r RemoteConfig) bool
AddOrUpdateRemote adds r to the file, or replaces the existing entry with the same name. The returned bool is true when an existing entry was replaced.
func (*RemotesFile) FindRemote ¶
func (rf *RemotesFile) FindRemote(name string) (RemoteConfig, error)
FindRemote returns the RemoteConfig with the given name, or an error wrapping os.ErrNotExist when no such remote is configured.
func (*RemotesFile) RemoveRemote ¶
func (rf *RemotesFile) RemoveRemote(name string) bool
RemoveRemote deletes the remote with the given name. The returned bool is true when an entry was removed.
type SMBFS ¶
type SMBFS struct {
// contains filtered or unexported fields
}
SMBFS implements RemoteFS over the SMB2/3 protocol using go-smb2. Unlike WebDAV (stateless HTTP), SMB holds a live TCP connection + session, so Close() must be called to release resources.
func (*SMBFS) Close ¶
Close releases the SMB session and TCP connection. The share is unmounted and the session logged off in order; conn.Close is called last and its error is ignored because Logoff typically closes the underlying transport, making a subsequent conn.Close return a benign "closed" error.
func (*SMBFS) Write ¶
Write uploads a file atomically: it first writes to <path>.partial, then removes the target and renames the partial onto it. This prevents a network interruption from leaving a half-written object on the remote. go-smb2's Rename does not support overwrite, so the target is removed first (a missing target is not an error).
type SyncOptions ¶
type SyncOptions struct {
// Concurrency is the maximum number of concurrent chunk transfers.
// A non-positive value falls back to defaultConcurrency (8).
Concurrency int
}
SyncOptions configures the behavior of Push, Pull, PushDryRun, and PullDryRun. A zero-value SyncOptions uses sensible defaults. Pass it explicitly to each call so that concurrent operations do not share mutable global state.
type SyncStats ¶
type SyncStats struct {
SnapshotsUploaded int
SnapshotsSkipped int
ManifestsUploaded int
ChunksUploaded int
ChunksSkipped int
RefsUpdated int
RefsDiverged int // pull only: refs saved as <name>.remote
IndexRebuilt bool
BranchTipChanged string // branch name whose tip advanced ("" if none)
}
SyncStats reports the outcome of a push or pull operation.
func Pull ¶
func Pull(ctx context.Context, store storage.Storer, rfs RemoteFS, branch string, opts SyncOptions) (*SyncStats, error)
Pull downloads remote objects to local. Objects already present locally are skipped. Diverged refs (same name, different target) are saved as <name>.remote locally. HEAD and config are NOT synced. After pulling, if the current branch tip changed, the local index is rebuilt.
If branch is non-empty, only that branch's snapshot chain and its chunks are synced, plus the branch ref itself.
func PullDryRun ¶
func PullDryRun(ctx context.Context, store storage.Storer, rfs RemoteFS, branch string, opts SyncOptions) (*SyncStats, error)
PullDryRun collects the pull scope and returns stats without actually downloading anything. The remote is only read (for listing).
func Push ¶
func Push(ctx context.Context, store storage.Storer, rfs RemoteFS, branch string, opts SyncOptions) (*SyncStats, error)
Push uploads local objects (chunks, snapshots, manifests, refs) to the remote. Objects already present on the remote are skipped. Refs that diverge (same name, different target) cause an error for that ref — the user must pull first. HEAD and config are NOT synced (see design doc §6.1).
Upload order is chunks → snapshots → manifests → refs. This guarantees that when a snapshot becomes visible on the remote, every chunk it references is already there (so a concurrent pull never sees a half-complete snapshot), and refs are updated last so a branch tip only ever points at a fully-uploaded object graph.
If branch is non-empty, only that branch's snapshot chain and its chunks are synced, plus the branch ref itself.
type WebDAVFS ¶
type WebDAVFS struct {
// contains filtered or unexported fields
}
WebDAVFS implements RemoteFS over the WebDAV protocol using gowebdav. The underlying *gowebdav.Client is safe for concurrent use.
func (*WebDAVFS) MkdirAll ¶
MkdirAll creates a directory tree. It stats first so the common "already exists" case avoids relying on the server's 405 response, which is not a reliable existence signal across WebDAV implementations.
func (*WebDAVFS) Write ¶
Write uploads a file atomically: it first writes to <path>.partial, then renames it onto the final path. This prevents a network interruption from leaving a half-written object on the remote that other clients would mistake for a complete content-addressed object. gowebdav's Rename supports overwrite so the final path is replaced cleanly.