Documentation
¶
Overview ¶
Package remote is the filesystem the sftp tab browses: either this machine or a host reached over SFTP, behind one interface so the panel above cannot tell the difference.
Both sides of the tab are the same type, which is what makes "upload", "download" and "remote to remote" one operation instead of three.
Index ¶
- Constants
- Variables
- func CopyItem(ctx context.Context, src, dst FS, it Item, onBytes func(int64)) error
- func Digest(p string) ([32]byte, error)
- func Exists(f FS, p string) bool
- func Fetch(ctx context.Context, f FS, p, dir string, onBytes func(int64)) (string, error)
- func Join(dir, name string) string
- func LocalPath(f FS, p string) (string, bool)
- func Parent(dir string) string
- func Peek(f FS, p string, n int) ([]byte, error)
- func RemoveAll(f FS, p string) error
- func SameTree(src FS, srcPath string, dst FS, dstDir string) bool
- func Scan(ctx context.Context, f FS, root string, dirs []string, limit int, ...) (capped bool)
- func StartDir(f FS, home string) string
- func WriteBack(f FS, src, p string, mode fs.FileMode) error
- type Entry
- type FS
- type HostKey
- type HostKeyPrompt
- type Item
- type Stamp
Constants ¶
const LocalLabel = "local"
LocalLabel is what the local side is called in the host picker and the panel title. A name, not a path: "local" is a place you understand without reading.
const PeekCap = 64 * 1024
PeekCap is how much of a file a preview reads. Every byte crosses the network on a remote side, so this is a transfer budget rather than a memory one: 64 KiB is enough to answer "is this the file I think it is" and small enough that pressing the key on a multi-gigabyte log is not a mistake you have to wait out.
const SearchCap = 20000
SearchCap bounds a scan, in the same spirit as planCap: past this many entries a search is not the tool you want, and the walk stops rather than holding the connection open indefinitely. The UI says when it stopped early, because a truncated result set that claims to be complete is worse than a slow one.
Variables ¶
var ErrPlanTooLarge = errors.New("too many files")
ErrPlanTooLarge says the walk hit the cap. The count is included so the message can be specific rather than "too many".
Functions ¶
func CopyItem ¶
CopyItem transfers one item, reporting bytes as they land.
A cancelled or failed file is REMOVED. A partial file that looks like the real one is the worst outcome here: the next thing that reads it gets truncated data with nothing to say it was truncated.
func Digest ¶
Digest is how "you edited this" is told apart from "you looked at it and quit". Not mtime: an editor rewrites the file on :w even when every byte is the same, so mtime answers a different question than the one being asked.
func Exists ¶
Exists reports whether p is there. Transfers use it to find overwrites before starting, so the question is asked once for the whole batch rather than per file, halfway through.
func Fetch ¶
Fetch copies p into dir under its own name and returns the local path.
The name is kept because editors read it: syntax rules, filetype detection and modelines all key off it, and nginx.conf opened as sshu-edit-417 is a different editing session than the one you asked for.
There is no size limit, deliberately. What edit needs is "never write back a partial read", and that is bought by reading the whole file or failing — not by refusing large ones. It streams, so size costs time and disk, never memory, and ctx is what makes the time cancellable.
func Join ¶
Join builds a child path. It is package-level rather than a method because every FS uses slash paths, so there is nothing per-implementation about it.
func LocalPath ¶
LocalPath answers "can an editor be pointed straight at this file?".
When it can, there is no copy at all. Editing in place keeps the inode, so hard links, ownership and extended attributes survive — a round trip through a temp file and a rename would break all three, and on this machine there is no network failure to buy protection from.
func Parent ¶
Parent is dir's parent, or dir itself at the root — going up from "/" must not walk off the end.
func Peek ¶
Peek reads at most n bytes from the start of p.
It is deliberately not Plan/CopyItem's path: nothing is written anywhere, and the caller wants the bytes in hand rather than streamed.
func RemoveAll ¶
RemoveAll deletes p, and everything under it if it is a directory.
It walks with Lstat and with the LISTING's IsDir, both of which report a symlink as a symlink. The link is unlinked; whatever it points at is left alone. Deleting the contents of a directory somebody merely linked to would be the worst kind of surprise this app could produce.
func SameTree ¶
SameTree reports whether copying src into dstDir on the same filesystem would put a directory inside itself, which walks forever.
func Scan ¶
func Scan(ctx context.Context, f FS, root string, dirs []string, limit int, emit func([]Entry)) (capped bool)
Scan walks dirs breadth-first and hands each directory's listing to emit, with every entry's Name rewritten as its path relative to root — so the caller can still build an absolute path with Join(root, Name) and does not have to know how deep a result came from.
Breadth-first is the whole point. Over SFTP each directory is a round trip, so the order results arrive in is the order the user waits in, and what someone is looking for is far more often two levels down than twenty. A depth-first walk would spend its first seconds inside whichever subtree happens to sort first, which from the outside is indistinguishable from being stuck.
A directory that will not list is skipped, not fatal. One unreadable directory is ordinary on someone else's machine, and dropping every result below it is not a reasonable answer to it.
It returns when the queue empties, when ctx is cancelled, or at limit; capped reports which of the last two ended it.
func StartDir ¶
StartDir is where a side should open when it connects.
A remote side opens at its home: there is nowhere else it could mean. THIS machine opens where sshu was launched, because that is the directory you were standing in when you decided you needed sshu — `cd ~/release && sshu` should already be looking at the release, not at a home directory full of dotfiles you then have to navigate out of.
sshu never changes its own working directory, so asking for it here is the same as having captured it at startup.
func WriteBack ¶
WriteBack replaces p's contents with the file at src.
The bytes go to a sibling name first and are then moved over the target, so the file is never seen half-written: a link that drops mid-upload leaves the original intact rather than a truncated config that the next process to read it will believe.
Two cases step around that, and both are deliberate:
- a SYMLINK is written through. Renaming over one replaces the link with a regular file — the dotfile you edited would quietly stop pointing at the repository it came from.
- a directory that will not take a new file (your file, somebody else's directory) falls back to writing in place, because the alternative is not being able to save at all.
Each trades the atomic replace for a short window where the file is incomplete. In both, that is the smaller harm.
Types ¶
type FS ¶
type FS interface {
// Label is what the panel title shows: "local", or the host's name.
Label() string
// Home is where a side starts.
Home() (string, error)
// List returns dir's entries, directories first then names, both
// case-insensitively — the order a person scans in, not the order the
// server happened to return.
List(dir string) ([]Entry, error)
// Stat is used to decide whether a path is a directory before entering it.
Stat(p string) (Entry, error)
// Lstat is Stat without following a symlink. Anything DESTRUCTIVE must use
// it: a symlink to a directory stats as a directory, and a recursive delete
// that believed Stat would walk into the target and empty it out.
Lstat(p string) (Entry, error)
// The write half, used only by transfers. It lives on the same interface so a
// copy never has to know which end is local — upload, download and
// remote-to-remote become one code path with different values.
Open(p string) (io.ReadCloser, error)
Create(p string, mode fs.FileMode) (io.WriteCloser, error)
MkdirAll(p string) error
Remove(p string) error
// Rename moves a path within this filesystem. Callers check the destination
// first: os.Rename would overwrite it and SFTP's would refuse, and one
// operation must not depend on which end it lands on.
Rename(from, to string) error
// Replace is the other half of that split: move from ONTO to, whatever is
// already there. It exists because an edit's write-back has to land in one
// step — a file seen half-written is a file the next process to read it
// believes.
Replace(from, to string) error
Close() error
}
FS is a browsable filesystem.
Paths are always slash-separated and absolute. The local implementation converts at its own boundary, so the UI never has to care which side it is talking to.
func Dial ¶
Dial opens an SFTP session to h.
The host key is checked against ~/.ssh/known_hosts. An unknown host goes to prompt with its fingerprint; a host whose key has CHANGED is refused outright and never offered — that is the case known_hosts exists to catch, and turning it into a yes/no question trains people to answer yes. Dial opens an SFTP connection. timeout bounds the attempt, so a host that will never answer cannot hold a panel open forever; it comes from config.yaml by way of the UI, because a person on a slow link and a person on a LAN want different numbers and neither of them should have to rebuild sshu.
func DialPipe ¶ added in v1.7.0
DialPipe speaks SFTP over a subprocess's stdin and stdout — the subprocess being `ssh -s <host> sftp`, which is how OpenSSH's own sftp client works.
It exists for the host whose auth is "sshconfig": everything about reaching that host — HostName, ProxyJump, the key, the agent, a passphrase — is in a file ssh reads and this package does not, so the only honest way to connect is to let ssh do it. Dial keeps speaking the protocol itself for the hosts whose secrets sshu holds; that path reads none of ~/.ssh/config, which is the asymmetry the Config panel already discloses, and it is not widened here — a host that told sshu its password did not ask ssh to get involved.
cmd is started here and owned by the returned FS: Close ends the subprocess. Whatever ssh printed to stderr is kept, and becomes the error text when the handshake fails — that is where ssh explains itself, in the words the Errors journal wants: "Permission denied", "Host key verification failed", "Connection timed out".
The caller decides what the subprocess IS, and that includes putting it in its own session (Setsid) so it cannot reach for the terminal sshu is drawing on: with no tty and SSH_ASKPASS set, every question ssh has goes to the helper instead. This function only knows that a session leader is killed as a group, so a ProxyJump's child goes with it.
type HostKey ¶ added in v1.5.0
type HostKey struct {
Type string
Key string // base64 — the wire format, exactly as known_hosts stores it
Fingerprint string
}
HostKey is what a server offered when asked to identify itself: the three things a known_hosts line needs, plus the fingerprint a person compares.
func ScanHostKey ¶ added in v1.5.0
ScanHostKey opens a connection far enough to see the host key and no further.
This is what `ssh-keyscan` does, and what ssh itself does on a first connect before it asks whether to trust the fingerprint. It exists because the alternative way to add a known host is to type 68 characters of base64 that somebody has to have got from somewhere else anyway — a form nobody would use, guarding a file that matters.
Nothing is written here. The caller shows the fingerprint and asks.
type HostKeyPrompt ¶
HostKeyPrompt is asked to approve a host that is not in known_hosts. Returning false aborts the connection.
It is a parameter rather than a policy baked in here because the decision is the user's, and this package has no way to ask them.
type Item ¶
Item is one thing to create at the far end. Directories are items too: an empty directory has to arrive, and a file cannot be written before its parent exists.
func Plan ¶
Plan expands paths into the items a transfer will create under dstDir, recursing into directories, and returns the total bytes.
It runs before anything is written so the whole shape of the job is known up front: how many files, how many bytes, and which of them already exist at the far end. Discovering an overwrite mid-copy would mean asking a question with half the batch already committed.