golang.org/x/build@v0.0.0-20240506185731-218518f32b70/perfdata/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 "context" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "io" 13 "mime/multipart" 14 "net/http" 15 "net/url" 16 "path/filepath" 17 "sort" 18 "strings" 19 "time" 20 21 "golang.org/x/build/perfdata/db" 22 "golang.org/x/perf/storage/benchfmt" 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 }