github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/api/receiver/receive_results.go (about) 1 // Copyright 2018 The WPT Dashboard Project. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package receiver 6 7 import ( 8 "fmt" 9 "mime/multipart" 10 "net/http" 11 "sync" 12 13 "github.com/google/uuid" 14 "github.com/web-platform-tests/wpt.fyi/shared" 15 ) 16 17 // BufferBucket is the GCS bucket to temporarily store results until they are proccessed. 18 const BufferBucket = "wptd-results-buffer" 19 20 // ResultsQueue is the name of the results processing TaskQueue. 21 const ResultsQueue = "results-arrival" 22 23 // ResultsTarget is the target URL for results processing tasks. 24 const ResultsTarget = "/api/results/process" 25 26 // HandleResultsUpload handles the POST requests for uploading results. 27 func HandleResultsUpload(a API, w http.ResponseWriter, r *http.Request) { 28 // Most form methods (e.g. FormValue) will call ParseMultipartForm and 29 // ParseForm if necessary; forms with either enctype can be parsed. 30 // FormValue gets either query params or form body entries, favoring 31 // the latter. 32 // The default maximum form size is 32MB, which is also the max request 33 // size on AppEngine. 34 35 var uploader string 36 if a.IsAdmin(r) { 37 uploader = r.FormValue("user") 38 if uploader == "" { 39 http.Error(w, "Please specify uploader", http.StatusBadRequest) 40 41 return 42 } 43 } else { 44 uploader = AuthenticateUploader(a, r) 45 if uploader == "" { 46 http.Error(w, "Authentication error", http.StatusUnauthorized) 47 48 return 49 } 50 } 51 52 // Non-existent keys will have empty values, which will later be 53 // filtered out by scheduleResultsTask. 54 extraParams := map[string]string{ 55 "labels": r.FormValue("labels"), 56 "callback_url": r.FormValue("callback_url"), 57 // The following fields will be deprecated when all runners embed metadata in the report. 58 "revision": r.FormValue("revision"), 59 "browser_name": r.FormValue("browser_name"), 60 "browser_version": r.FormValue("browser_version"), 61 "os_name": r.FormValue("os_name"), 62 "os_version": r.FormValue("os_version"), 63 } 64 65 log := shared.GetLogger(a.Context()) 66 var results, screenshots []string 67 if f := r.MultipartForm; f != nil && f.File != nil && len(f.File["result_file"]) > 0 { 68 // result_file[] payload 69 files := f.File["result_file"] 70 sFiles := f.File["screenshot_file"] 71 log.Debugf("Found %d result files, %d screenshot files", results, screenshots) 72 var err error 73 results, screenshots, err = saveToGCS(a, uploader, files, sFiles) 74 if err != nil { 75 log.Errorf("Failed to save files to GCS: %v", err) 76 http.Error(w, "Failed to save files to GCS", http.StatusInternalServerError) 77 78 return 79 } 80 } else if artifactName := getAzureArtifactName(r.PostForm.Get("result_url")); artifactName != "" { 81 // Special Azure case for result_url payload 82 azureURL := r.PostForm.Get("result_url") 83 log.Debugf("Found Azure URL: %s", azureURL) 84 extraParams["azure_url"] = azureURL 85 } else if len(r.PostForm["result_url"]) > 0 { 86 // General result_url[] payload 87 results = r.PostForm["result_url"] 88 screenshots = r.PostForm["screenshot_url"] 89 log.Debugf("Found %d result URLs, %d screenshot URLs", results, screenshots) 90 } else { 91 log.Errorf("No results found") 92 http.Error(w, "No results found", http.StatusBadRequest) 93 94 return 95 } 96 97 t, err := a.ScheduleResultsTask(uploader, results, screenshots, extraParams) 98 if err != nil { 99 log.Errorf("Failed to schedule task: %v", err) 100 http.Error(w, "Failed to schedule task", http.StatusInternalServerError) 101 102 return 103 } 104 log.Infof("Task %s added to queue", t) 105 fmt.Fprintf(w, "Task %s added to queue\n", t) 106 } 107 108 func saveToGCS(a API, uploader string, resultFiles, screenshotFiles []*multipart.FileHeader) ( 109 resultGCS, screenshotGCS []string, err error) { 110 id := uuid.New() 111 resultGCS = make([]string, len(resultFiles)) 112 screenshotGCS = make([]string, len(screenshotFiles)) 113 for i := range resultFiles { 114 resultGCS[i] = fmt.Sprintf("gs://%s/%s/%s/%d.json", BufferBucket, uploader, id, i) 115 } 116 for i := range screenshotFiles { 117 screenshotGCS[i] = fmt.Sprintf("gs://%s/%s/%s/%d.db", BufferBucket, uploader, id, i) 118 } 119 120 var wg sync.WaitGroup 121 moveFile := func(errors chan error, file *multipart.FileHeader, gcsPath string) { 122 defer wg.Done() 123 f, err := file.Open() 124 if err != nil { 125 errors <- err 126 127 return 128 } 129 defer f.Close() 130 // nolint:godox // TODO(Hexcles): Detect whether the file is gzipped. 131 // nolint:godox // TODO(Hexcles): Retry after failures. 132 if err := a.UploadToGCS(gcsPath, f, true); err != nil { 133 errors <- err 134 } 135 } 136 137 errors1 := make(chan error, len(resultFiles)) 138 errors2 := make(chan error, len(screenshotFiles)) 139 wg.Add(len(resultFiles) + len(screenshotFiles)) 140 for i, gcsPath := range resultGCS { 141 moveFile(errors1, resultFiles[i], gcsPath) 142 } 143 for i, gcsPath := range screenshotGCS { 144 moveFile(errors2, screenshotFiles[i], gcsPath) 145 } 146 wg.Wait() 147 close(errors1) 148 close(errors2) 149 150 mErr := shared.NewMultiErrorFromChan(errors1, fmt.Sprintf("storing results from %s to GCS", uploader)) 151 if mErr != nil { 152 // Result errors are fatal. 153 shared.GetLogger(a.Context()).Errorf(mErr.Error()) 154 155 return nil, nil, mErr 156 } 157 mErr = shared.NewMultiErrorFromChan(errors2, fmt.Sprintf("storing screenshots from %s to GCS", uploader)) 158 if mErr != nil { 159 // Screenshot errors are not fatal. 160 shared.GetLogger(a.Context()).Warningf(mErr.Error()) 161 screenshotGCS = nil 162 } 163 164 return resultGCS, screenshotGCS, nil 165 }