github.com/jgbaldwinbrown/perf@v0.1.1/storage/app/upload.go (about)

     1  // Copyright 2016 The Go Authors.  All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package app
     6  
     7  import (
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"mime/multipart"
    13  	"net/http"
    14  	"net/url"
    15  	"path/filepath"
    16  	"sort"
    17  	"strings"
    18  	"time"
    19  
    20  	"golang.org/x/net/context"
    21  	"golang.org/x/perf/storage/benchfmt"
    22  	"golang.org/x/perf/storage/db"
    23  )
    24  
    25  // upload is the handler for the /upload endpoint. It serves a form on
    26  // GET requests and processes files in a multipart/x-form-data POST
    27  // request.
    28  func (a *App) upload(w http.ResponseWriter, r *http.Request) {
    29  	ctx := requestContext(r)
    30  
    31  	user, err := a.Auth(w, r)
    32  	switch {
    33  	case err == ErrResponseWritten:
    34  		return
    35  	case err != nil:
    36  		errorf(ctx, "%v", err)
    37  		http.Error(w, err.Error(), 500)
    38  		return
    39  	}
    40  
    41  	if r.Method == http.MethodGet {
    42  		http.ServeFile(w, r, filepath.Join(a.BaseDir, "static/upload.html"))
    43  		return
    44  	}
    45  	if r.Method != http.MethodPost {
    46  		http.Error(w, "/upload must be called as a POST request", http.StatusMethodNotAllowed)
    47  		return
    48  	}
    49  
    50  	// We use r.MultipartReader instead of r.ParseForm to avoid
    51  	// storing uploaded data in memory.
    52  	mr, err := r.MultipartReader()
    53  	if err != nil {
    54  		errorf(ctx, "%v", err)
    55  		http.Error(w, err.Error(), 500)
    56  		return
    57  	}
    58  
    59  	result, err := a.processUpload(ctx, user, mr)
    60  	if err != nil {
    61  		errorf(ctx, "%v", err)
    62  		http.Error(w, err.Error(), 500)
    63  		return
    64  	}
    65  
    66  	w.Header().Set("Content-Type", "application/json")
    67  	if err := json.NewEncoder(w).Encode(result); err != nil {
    68  		errorf(ctx, "%v", err)
    69  		http.Error(w, err.Error(), 500)
    70  		return
    71  	}
    72  }
    73  
    74  // uploadStatus is the response to an /upload POST served as JSON.
    75  type uploadStatus struct {
    76  	// UploadID is the upload ID assigned to the upload.
    77  	UploadID string `json:"uploadid"`
    78  	// FileIDs is the list of file IDs assigned to the files in the upload.
    79  	FileIDs []string `json:"fileids"`
    80  	// ViewURL is a URL that can be used to interactively view the upload.
    81  	ViewURL string `json:"viewurl,omitempty"`
    82  }
    83  
    84  // processUpload takes one or more files from a multipart.Reader,
    85  // writes them to the filesystem, and indexes their content.
    86  func (a *App) processUpload(ctx context.Context, user string, mr *multipart.Reader) (*uploadStatus, error) {
    87  	var upload *db.Upload
    88  	var fileids []string
    89  
    90  	uploadtime := time.Now().UTC().Format(time.RFC3339)
    91  
    92  	for i := 0; ; i++ {
    93  		p, err := mr.NextPart()
    94  		if err == io.EOF {
    95  			break
    96  		}
    97  		if err != nil {
    98  			return nil, err
    99  		}
   100  
   101  		name := p.FormName()
   102  		if name == "commit" {
   103  			continue
   104  		}
   105  		if name != "file" {
   106  			return nil, fmt.Errorf("unexpected field %q", name)
   107  		}
   108  
   109  		if upload == nil {
   110  			var err error
   111  			upload, err = a.DB.NewUpload(ctx)
   112  			if err != nil {
   113  				return nil, err
   114  			}
   115  			defer func() {
   116  				if upload != nil {
   117  					upload.Abort()
   118  				}
   119  			}()
   120  		}
   121  
   122  		// The incoming file needs to be stored in Cloud
   123  		// Storage and it also needs to be indexed. If the file
   124  		// is invalid (contains no valid records) it needs to
   125  		// be rejected and the Cloud Storage upload aborted.
   126  
   127  		meta := map[string]string{
   128  			"upload":      upload.ID,
   129  			"upload-part": fmt.Sprintf("%s/%d", upload.ID, i),
   130  			"upload-time": uploadtime,
   131  		}
   132  		name = p.FileName()
   133  		if slash := strings.LastIndexAny(name, `/\`); slash >= 0 {
   134  			name = name[slash+1:]
   135  		}
   136  		if name != "" {
   137  			meta["upload-file"] = name
   138  		}
   139  		if user != "" {
   140  			meta["by"] = user
   141  		}
   142  
   143  		// We need to do two things with the incoming data:
   144  		// - Write it to permanent storage via a.FS
   145  		// - Write index records to a.DB
   146  		// AND if anything fails, attempt to clean up both the
   147  		// FS and the index records.
   148  
   149  		if err := a.indexFile(ctx, upload, p, meta); err != nil {
   150  			return nil, err
   151  		}
   152  
   153  		fileids = append(fileids, meta["upload-part"])
   154  	}
   155  
   156  	if upload == nil {
   157  		return nil, errors.New("no files processed")
   158  	}
   159  	if err := upload.Commit(); err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	status := &uploadStatus{UploadID: upload.ID, FileIDs: fileids}
   164  	if a.ViewURLBase != "" {
   165  		status.ViewURL = a.ViewURLBase + url.QueryEscape(upload.ID)
   166  	}
   167  
   168  	upload = nil
   169  
   170  	return status, nil
   171  }
   172  
   173  func (a *App) indexFile(ctx context.Context, upload *db.Upload, p io.Reader, meta map[string]string) (err error) {
   174  	path := fmt.Sprintf("uploads/%s.txt", meta["upload-part"])
   175  	fw, err := a.FS.NewWriter(ctx, path, meta)
   176  	if err != nil {
   177  		return err
   178  	}
   179  	defer func() {
   180  		start := time.Now()
   181  		if err != nil {
   182  			fw.CloseWithError(err)
   183  		} else {
   184  			err = fw.Close()
   185  		}
   186  		infof(ctx, "Close(%q) took %.2f seconds", path, time.Since(start).Seconds())
   187  	}()
   188  	var keys []string
   189  	for k := range meta {
   190  		keys = append(keys, k)
   191  	}
   192  	sort.Strings(keys)
   193  	for _, k := range keys {
   194  		if _, err := fmt.Fprintf(fw, "%s: %s\n", k, meta[k]); err != nil {
   195  			return err
   196  		}
   197  	}
   198  	// Write a blank line to separate metadata from user-generated content.
   199  	fmt.Fprintf(fw, "\n")
   200  
   201  	// TODO(quentin): Add a separate goroutine and buffer for writes to fw?
   202  	tr := io.TeeReader(p, fw)
   203  	br := benchfmt.NewReader(tr)
   204  	br.AddLabels(meta)
   205  	i := 0
   206  	for br.Next() {
   207  		i++
   208  		if err := upload.InsertRecord(br.Result()); err != nil {
   209  			return err
   210  		}
   211  	}
   212  	if err := br.Err(); err != nil {
   213  		return err
   214  	}
   215  	if i == 0 {
   216  		return errors.New("no valid benchmark lines found")
   217  	}
   218  	return nil
   219  }