Documentation
¶
Overview ¶
Package filetransfer implements file filtering and SFTP upload for docker-deploy.
Index ¶
- Variables
- func ShellQuote(s string) string
- func ShouldExclude(relPath string, excludes []string) bool
- func SudoExec(client *gossh.Client, cmd string, creds *SudoCreds, warnedOnce *bool, ...) error
- func Upload(ctx context.Context, client *gossh.Client, localDir, remoteBase string, ...) (int, error)
- func WalkFiles(localDir string, excludes []string) ([]string, error)
- type SudoCreds
Constants ¶
This section is empty.
Variables ¶
var ErrDeployCancelled = errors.New("deploy cancelled by user")
ErrDeployCancelled is returned by Upload() when the user declines the repeat-deploy confirmation prompt. Callers should treat this as a clean exit (print "Deploy cancelled." and return nil), not as an error.
Functions ¶
func ShellQuote ¶
ShellQuote wraps s in single quotes for safe use in shell commands, escaping any embedded single quotes using the '\" technique. This handles paths derived from validated config values (remoteBase is from Resolve() which validates via ParseHost; staging dir name uses only alphanumerics + timestamp integer — T-03-05).
func ShouldExclude ¶
ShouldExclude reports whether relPath (relative to the walk root, using forward slashes) matches any pattern in excludes.
Pattern matching rules:
- Directory patterns ending in "/" match: the exact directory name, any path that starts with the pattern, or any path component matching the directory name (to handle deep paths like "a/node_modules/b").
- Other patterns use filepath.Match semantics against both the basename and the full path.
func SudoExec ¶ added in v0.9.0
func SudoExec(client *gossh.Client, cmd string, creds *SudoCreds, warnedOnce *bool, verbose bool) error
SudoExec runs cmd on the remote with automatic privilege escalation fallback.
Step order (D-11):
- Direct: sshRun(client, cmd, nil) — no sudo
- Cached: if creds.pw != nil, sshRun with creds.pw
- Passwordless: sudo -n sh -c <cmd> (NOPASSWD sudoers) 3b. Keychain: if creds.Host/User are set, check macOS Keychain for stored password
- Interactive: prompt up to 3 times; on success cache password in creds.pw; offer to save to macOS Keychain if creds.Host/User are set
creds and warnedOnce are in/out params shared across all SudoExec calls within a single Upload() — ensures the interactive prompt fires at most once per deploy (SC-6, D-12).
func Upload ¶
func Upload(ctx context.Context, client *gossh.Client, localDir, remoteBase string, excludes []string, creds *SudoCreds, force bool, warnedOnce *bool, verbose bool) (int, error)
Upload copies all non-excluded files from localDir to a staging directory on the remote, then atomically renames the staging dir to the final target.
remoteBase is the target directory path on the remote (e.g. "/opt/myapp"). excludes is the list of exclude patterns (from Config.Excludes). force skips the replace-confirmation prompt on repeat deploys. verbose controls per-file output and SSH command logging:
- When verbose=true: per-file lines " -> relative/path" are written to os.Stderr; each SSH command (mkdir, mv, rm) and its exit code are logged to os.Stderr before/after execution; warnedOnce is never set to true so every sudo warning prints. When !force and existsBefore, the pre-confirm diff block prints local and remote file lists (truncated at 20) before the "Replace all contents?" prompt.
- When verbose=false: per-file lines are suppressed; SSH command lines are suppressed; warnedOnce behavior is unchanged (first warning prints, rest are suppressed).
Atomic swap strategy:
- Create staging dir: /tmp/docker-deploy-<unixNanoTimestamp> (/tmp is always writable by the SSH user via SFTP, even when target is root-owned)
- Upload all files into staging dir maintaining relative path structure
- Ensure remoteBase exists (mkdir -p, falling back to interactive sudo with up to 3 password attempts). If target cannot be created: warn, leave staged files, return error.
- Via SSH session exec: if remoteBase exists, mv to .old-<timestamp>, mv staging to remoteBase, rm -rf the .old dir; if absent, just mv staging to remoteBase
If WalkFiles returns 0 files, Upload returns an error.
Progress: prints "Uploading N files..." before starting to os.Stdout. Per-file lines go to os.Stderr only when verbose=true.
Per CLAUDE.md: sessions are NOT reusable — each SSH exec uses a fresh NewSession(). SFTP wraps the existing *gossh.Client — no second TCP connection.
Returns the number of files actually transferred on success.
Types ¶
type SudoCreds ¶ added in v0.9.0
type SudoCreds struct {
Host string // remote hostname (e.g. "myserver.example.com")
User string // SSH/sudo username (e.g. "deploy")
// contains filtered or unexported fields
}
SudoCreds holds a sudo password as bytes so it can be zeroed after use. The zero value (nil pw) means no password has been captured yet.
Host and User identify the remote target for macOS Keychain operations. When both are non-empty, SudoExec will check the Keychain before prompting interactively and offer to save a newly entered password after first use. Leaving them empty disables Keychain integration and preserves the existing behaviour (direct → cached → passwordless sudo → interactive prompt).