gomote, buildlet: add ls, push

Add gomote and buildlet support for listing remote files, and
efficiently syncing from the local workstation to the remote buildlet.

Change-Id: Ifab1fb1c208ca4bc66f8d6916c38e1914001a3a5
Reviewed-on: https://go-review.googlesource.com/4270
Reviewed-by: Andrew Gerrand <adg@golang.org>
diff --git a/buildlet/buildletclient.go b/buildlet/buildletclient.go
index 6f75506..adcb475 100644
--- a/buildlet/buildletclient.go
+++ b/buildlet/buildletclient.go
@@ -7,6 +7,7 @@
 package buildlet // import "golang.org/x/build/buildlet"
 
 import (
+	"bufio"
 	"errors"
 	"fmt"
 	"io"
@@ -152,6 +153,10 @@
 	// the buildlet's environment.
 	SystemLevel bool
 
+	// Debug, if true, instructs to the buildlet to print extra debug
+	// info to the output before the command begins executing.
+	Debug bool
+
 	// OnStartExec is an optional hook that runs after the 200 OK
 	// response from the buildlet, but before the output begins
 	// writing to Output.
@@ -175,6 +180,7 @@
 		"mode":   {mode},
 		"cmdArg": opts.Args,
 		"env":    opts.ExtraEnv,
+		"debug":  {fmt.Sprint(opts.Debug)},
 	}
 	req, err := http.NewRequest("POST", c.URL()+"/exec", strings.NewReader(form.Encode()))
 	if err != nil {
@@ -227,6 +233,9 @@
 
 // RemoveAll deletes the provided paths, relative to the work directory.
 func (c *Client) RemoveAll(paths ...string) error {
+	if len(paths) == 0 {
+		return nil
+	}
 	form := url.Values{"path": paths}
 	req, err := http.NewRequest("POST", c.URL()+"/removeall", strings.NewReader(form.Encode()))
 	if err != nil {
@@ -301,6 +310,79 @@
 	return string(b), nil
 }
 
+// DirEntry is the information about a file on a buildlet.
+type DirEntry struct {
+	// line is of the form "drw-rw-rw\t<name>" and then if a regular file,
+	// also "\t<size>\t<modtime>". in either case, without trailing newline.
+	// TODO: break into parsed fields?
+	line string
+}
+
+func (de DirEntry) String() string {
+	return de.line
+}
+
+func (de DirEntry) Name() string {
+	f := strings.Split(de.line, "\t")
+	if len(f) < 2 {
+		return ""
+	}
+	return f[1]
+}
+
+func (de DirEntry) Digest() string {
+	f := strings.Split(de.line, "\t")
+	if len(f) < 5 {
+		return ""
+	}
+	return f[4]
+}
+
+// ListDirOpts are options for Client.ListDir.
+type ListDirOpts struct {
+	// Recursive controls whether the directory is listed
+	// recursively.
+	Recursive bool
+
+	// Skip are the directories to skip, relative to the directory
+	// passed to ListDir. Each item should contain only forward
+	// slashes and not start or end in slashes.
+	Skip []string
+
+	// Digest controls whether the SHA-1 digests of regular files
+	// are returned.
+	Digest bool
+}
+
+// ListDir lists the contents of a directory.
+// The fn callback is run for each entry.
+func (c *Client) ListDir(dir string, opts ListDirOpts, fn func(DirEntry)) error {
+	param := url.Values{
+		"dir":       {dir},
+		"recursive": {fmt.Sprint(opts.Recursive)},
+		"skip":      opts.Skip,
+		"digest":    {fmt.Sprint(opts.Digest)},
+	}
+	req, err := http.NewRequest("GET", c.URL()+"/ls?"+param.Encode(), nil)
+	if err != nil {
+		return err
+	}
+	resp, err := c.do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusOK {
+		return errors.New(resp.Status)
+	}
+	sc := bufio.NewScanner(resp.Body)
+	for sc.Scan() {
+		line := strings.TrimSpace(sc.Text())
+		fn(DirEntry{line: line})
+	}
+	return sc.Err()
+}
+
 func condRun(fn func()) {
 	if fn != nil {
 		fn()