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  }