remote

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
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.

View Source
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.

View Source
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

func HostFromURL(rawURL string) (string, error)

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 LsRemote

func LsRemote(ctx context.Context, rfs RemoteFS) ([]*core.Reference, error)

LsRemote lists all refs on a remote without downloading objects.

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

type RemoteInfo struct {
	Path    string
	Size    int64
	IsDir   bool
	ModTime time.Time
}

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

func (s *SMBFS) Close() error

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) List

func (s *SMBFS) List(ctx context.Context, p string) ([]RemoteInfo, error)

List enumerates entries directly under a directory path (non-recursive).

func (*SMBFS) MkdirAll

func (s *SMBFS) MkdirAll(ctx context.Context, p string) error

MkdirAll creates a directory tree.

func (*SMBFS) Read

func (s *SMBFS) Read(ctx context.Context, p string) (io.ReadCloser, error)

Read opens a remote file for streaming. The caller must close the reader.

func (*SMBFS) Remove

func (s *SMBFS) Remove(ctx context.Context, p string) error

Remove deletes a remote file. A missing file is not an error.

func (*SMBFS) Stat

func (s *SMBFS) Stat(ctx context.Context, p string) (*RemoteInfo, error)

Stat returns metadata for a remote path.

func (*SMBFS) Write

func (s *SMBFS) Write(ctx context.Context, p string, r io.Reader) error

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.

func PushDryRun

func PushDryRun(ctx context.Context, store storage.Storer, rfs RemoteFS, branch string, opts SyncOptions) (*SyncStats, error)

PushDryRun collects the push scope and returns stats without actually uploading anything. The remote is only read (for existence checks).

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) Close

func (w *WebDAVFS) Close() error

Close is a no-op for WebDAV (stateless HTTP).

func (*WebDAVFS) List

func (w *WebDAVFS) List(ctx context.Context, p string) ([]RemoteInfo, error)

List enumerates entries directly under a directory path (non-recursive).

func (*WebDAVFS) MkdirAll

func (w *WebDAVFS) MkdirAll(ctx context.Context, p string) error

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) Read

func (w *WebDAVFS) Read(ctx context.Context, p string) (io.ReadCloser, error)

Read opens a remote file for streaming. The caller must close the reader.

func (*WebDAVFS) Remove

func (w *WebDAVFS) Remove(ctx context.Context, p string) error

Remove deletes a remote file. A missing file is not an error.

func (*WebDAVFS) Stat

func (w *WebDAVFS) Stat(ctx context.Context, p string) (*RemoteInfo, error)

Stat returns metadata for a remote path.

func (*WebDAVFS) Write

func (w *WebDAVFS) Write(ctx context.Context, p string, r io.Reader) error

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.

Jump to

Keyboard shortcuts

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