func (o *localOutput) URI() string {
	return o.uri
}
URI returns a URI to this Output
package runners
import (
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"github.com/twitter/scoot/runner"
	osexecer "github.com/twitter/scoot/runner/execer/os"
)
type HttpOutputCreator interface {
	http.Handler
	runner.OutputCreator
	HttpPath() string
}
type localOutputCreator struct {
	tmp      string
	hostname string
	httpUri  string
	httpPath string
	pathMap  map[string]string
}
func NewHttpOutputCreator(httpUri string) (HttpOutputCreator, error) {
	hostname, err := os.Hostname()
	if err != nil {
		return nil, err
	}
	httpPath := ""
	if httpUri != "" {
		u, err := url.Parse(httpUri)
		if err != nil {
			return nil, err
		}
		httpPath = strings.TrimSuffix(u.Path, "/") + "/"
	}
	tmp, err := ioutil.TempDir("", "output")
	if err != nil {
		return nil, err
	}
	return &localOutputCreator{
		tmp: tmp, hostname: hostname,
		httpUri: httpUri, httpPath: httpPath,
		pathMap: make(map[string]string),
	}, nil
}
func (s *localOutputCreator) Create(id string) (runner.Output, error) {
	if _, err := os.Stat(s.tmp); os.IsNotExist(err) {
		err = os.MkdirAll(s.tmp, os.ModePerm)
		if err != nil {
			return nil, err
		}
	}
	f, err := ioutil.TempFile(s.tmp, id)
	if err != nil {
		return nil, err
	}
	absPath, err := filepath.Abs(f.Name())
	if err != nil {
		return nil, err
	}
	uri := fmt.Sprintf("file://%s%s", s.hostname, absPath)
	if s.httpUri != "" {
		uri = fmt.Sprintf("%s/%s?file=%s", s.httpUri, id, uri)
		s.pathMap[strings.Trim(id, "/")] = absPath
		s.pathMap[filepath.Base(absPath)] = absPath
	}
	return &localOutput{f: f, absPath: absPath, uri: uri}, nil
}
func (s *localOutputCreator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	clientHtml :=
		`<html>
		<script type="text/javascript">
			let resourceId = "";
			const oneHour = 60 * 60 * 1000;
			const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
			const checkAtBottom = () => {
				let scrollTop =
					(document.documentElement && document.documentElement.scrollTop) ||
					document.body.scrollTop;
				let scrollHeight =
					(document.documentElement && document.documentElement.scrollHeight) ||
					document.body.scrollHeight;
				return scrollTop + window.innerHeight >= scrollHeight;
			};
			const gotoBottom = () => {
				let scrollHeight =
					(document.documentElement && document.documentElement.scrollHeight) ||
					document.body.scrollHeight;
				let scrollLeft =
					(document.documentElement && document.documentElement.scrollLeft) ||
					document.body.scrollLeft;
				window.scrollTo(scrollLeft, scrollHeight);
			};
			const parseLength = resp => {
				let contentRange = resp.headers.get("Content-Range");
				let idx = contentRange.lastIndexOf("/") + 1;
				return contentRange.slice(idx);
			};
			const copyBuffer = (oldBuffer, length) => {
				buffer = new Uint8Array(length);
				buffer.set(oldBuffer);
				return {
					buffer,
					length
				};
			};
			const writeBuffer = (mainBuffer, arrayBuffer, offset) => {
				let bytes = new Uint8Array(arrayBuffer);
				for (let i = 0; i < bytes.byteLength; i++) {
					mainBuffer[i + offset] = bytes[i];
				}
			};
			const updateText = buffer => {
				let wasAtBottom = checkAtBottom();
				let div = document.getElementById("output");
				div.innerText = new TextDecoder("utf-8").decode(buffer);
				if (wasAtBottom) {
					gotoBottom();
				}
			};
			const increaseTimeout = currTimeout => Math.trunc((currTimeout * 3) / 2);
			sendRequest = async () => {
				let url =
					location.href + (location.search == "" ? "?" : "&") + "content=true";
				let resp = await fetch(url, {
					method: "HEAD"
				});
				let length = Number.parseInt(resp.headers.get("Content-Length"));
				let buffer = new Uint8Array(length);
				let curr = 0;
				let offset = 1310720;
				let minTimeout = 50;
				let currTimeout = minTimeout;
				let maxTimeout = 15 * 60 * 1000;
				while (true) {
					let next = Math.min(curr + offset, length);
					if (curr == next) {
						let resp = await fetch(url, {
							method: "HEAD"
						});
						length = Number.parseInt(resp.headers.get("Content-Length"));
						lastModified = Date.parse(resp.headers.get("Last-Modified"));
						if (Date.now() - lastModified > oneHour) {
							break;
						}
						currTimeout = increaseTimeout(currTimeout);
					} else {
						resp = await fetch(url, {
							headers: new Headers({
								Range: "bytes=" + curr + "-" + next
							})
						});
						if (200 <= resp.status && resp.status < 300) {
							let id = resp.headers.get("X-Resource-Id");
							let newLength = parseLength(resp);
							if (newLength != length) {
								minTimeout = 5000;
								({ buffer, length } = copyBuffer(buffer, newLength));
							}
							writeBuffer(buffer, await resp.arrayBuffer(), curr);
							updateText(buffer);
							if (resourceId == "") {
								resourceId = id;
							}
							if (id != resourceId) {
								alert("Underlying resource changed! Quitting");
								break;
							}
							currTimeout = minTimeout;
							curr = next;
						} else {
							currTimeout = increaseTimeout(currTimeout);
						}
					}
					await sleep(Math.min(currTimeout, maxTimeout));
				}
			};
			sendRequest();
		</script>
		<body>
			<div id="output" style="white-space: pre-wrap"></div>
		</body>
	</html>	
`
	if strings.TrimSuffix(r.URL.Path, "/")+"/" == s.HttpPath() {
		http.StripPrefix(s.HttpPath(), http.FileServer(http.Dir(s.tmp))).ServeHTTP(w, r)
		return
	}
	path := strings.TrimPrefix(r.URL.Path, s.HttpPath())
	filepath, ok := s.pathMap[path]
	if !ok {
		http.Error(w, "Unrecognized path", http.StatusNotFound)
	} else if resource, err := os.Open(filepath); err != nil {
		http.Error(w, "", http.StatusGone)
	} else if info, err := resource.Stat(); err != nil {
		http.Error(w, "", http.StatusInternalServerError)
	} else {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("X-Resource-Id", filepath)
		if r.URL.Query().Get("content") == "true" {
			w.Header().Set("Cache-Control", "no-store")
			http.ServeContent(w, r, "", info.ModTime(), resource)
		} else {
			fmt.Fprintf(w, clientHtml)
		}
	}
}
func (s *localOutputCreator) HttpPath() string {
	return s.httpPath
}
type localOutput struct {
	f       *os.File
	absPath string
	uri     string
}