storage: support local disk for uploads

This adds a -data flag to localperfdata to specify a local data
directory. In combination with -dsn to specify a local database,
localperfdata is now useful for non-testing applications.

Change-Id: I32ddb7ba5a96483c1f93cb7b52bbc2643b5b3798
Reviewed-on: https://go-review.googlesource.com/37861
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/storage/fs/local/local.go b/storage/fs/local/local.go
new file mode 100644
index 0000000..650e6e3
--- /dev/null
+++ b/storage/fs/local/local.go
@@ -0,0 +1,48 @@
+// Copyright 2017 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package local implements the fs.FS interface using local files.
+// Metadata is not stored separately; the header of each file should
+// contain metadata as written by storage/app.
+package local
+
+import (
+	"os"
+	"path/filepath"
+
+	"golang.org/x/net/context"
+	"golang.org/x/perf/storage/fs"
+)
+
+// impl is an fs.FS backed by local disk.
+type impl struct {
+	root string
+}
+
+// NewFS constructs an FS that writes to the provided directory.
+func NewFS(root string) fs.FS {
+	return &impl{root}
+}
+
+// NewWriter creates a file and assigns metadata as extended filesystem attributes.
+func (fs *impl) NewWriter(ctx context.Context, name string, metadata map[string]string) (fs.Writer, error) {
+	if err := os.MkdirAll(filepath.Join(fs.root, filepath.Dir(name)), 0777); err != nil {
+		return nil, err
+	}
+	f, err := os.Create(filepath.Join(fs.root, name))
+	if err != nil {
+		return nil, err
+	}
+	return &wrapper{f}, nil
+}
+
+type wrapper struct {
+	*os.File
+}
+
+// CloseWithError closes the file and attempts to unlink it.
+func (w *wrapper) CloseWithError(error) error {
+	w.Close()
+	return os.Remove(w.Name())
+}
diff --git a/storage/fs/local/local_test.go b/storage/fs/local/local_test.go
new file mode 100644
index 0000000..b34ab23
--- /dev/null
+++ b/storage/fs/local/local_test.go
@@ -0,0 +1,50 @@
+// Copyright 2017 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package local
+
+import (
+	"context"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"golang.org/x/perf/internal/diff"
+)
+
+func TestNewWriter(t *testing.T) {
+	ctx := context.Background()
+
+	dir, err := ioutil.TempDir("", "local_test")
+	if err != nil {
+		t.Fatalf("TempDir = %v", err)
+	}
+	defer os.RemoveAll(dir)
+
+	fs := NewFS(dir)
+
+	w, err := fs.NewWriter(ctx, "dir/file", map[string]string{"key": "value", "key2": "value2"})
+	if err != nil {
+		t.Fatalf("NewWriter = %v", err)
+	}
+
+	want := "hello world"
+
+	if _, err := w.Write([]byte(want)); err != nil {
+		t.Fatalf("Write = %v", err)
+	}
+
+	if err := w.Close(); err != nil {
+		t.Fatalf("Close = %v", err)
+	}
+
+	have, err := ioutil.ReadFile(filepath.Join(dir, "dir/file"))
+	if err != nil {
+		t.Fatalf("ReadFile = %v", err)
+	}
+	if d := diff.Diff(string(have), want); d != "" {
+		t.Errorf("file contents differ. have (-)/want (+)\n%s", d)
+	}
+}
diff --git a/storage/localperfdata/app.go b/storage/localperfdata/app.go
index 9a8d560..8e90214 100644
--- a/storage/localperfdata/app.go
+++ b/storage/localperfdata/app.go
@@ -19,12 +19,14 @@
 	"golang.org/x/perf/storage/db"
 	_ "golang.org/x/perf/storage/db/sqlite3"
 	"golang.org/x/perf/storage/fs"
+	"golang.org/x/perf/storage/fs/local"
 )
 
 var (
 	addr        = flag.String("addr", ":8080", "serve HTTP on `address`")
 	viewURLBase = flag.String("view_url_base", "", "/upload response with `URL` for viewing")
 	dsn         = flag.String("dsn", ":memory:", "sqlite `dsn`")
+	data        = flag.String("data", "", "data `directory` (in-memory if empty)")
 	baseDir     = flag.String("base_dir", basedir.Find("golang.org/x/perf/storage/appengine"), "base `directory` for static files")
 )
 
@@ -40,7 +42,11 @@
 	if err != nil {
 		log.Fatalf("open database: %v", err)
 	}
-	fs := fs.NewMemFS()
+	var fs fs.FS = fs.NewMemFS()
+
+	if *data != "" {
+		fs = local.NewFS(*data)
+	}
 
 	app := &app.App{
 		DB:          db,