github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/api/handlers.go (about)

     1  package api
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net/http"
     9  
    10  	"github.com/qri-io/dataset"
    11  	"github.com/qri-io/qri/api/util"
    12  	"github.com/qri-io/qri/base/archive"
    13  	"github.com/qri-io/qri/event"
    14  	"github.com/qri-io/qri/lib"
    15  )
    16  
    17  const (
    18  	// maxBodyFileSize is 100MB
    19  	maxBodyFileSize = 100 << 20
    20  )
    21  
    22  // GetBodyCSVHandler is a handler for returning the body as a csv file
    23  // Examples:
    24  // curl http://localhost:2503/ds/get/b5/world_bank_population/body.csv
    25  func GetBodyCSVHandler(inst *lib.Instance) http.HandlerFunc {
    26  	return func(w http.ResponseWriter, r *http.Request) {
    27  		if r.Method != http.MethodGet {
    28  			util.NotFoundHandler(w, r)
    29  			return
    30  		}
    31  
    32  		p := &lib.GetParams{}
    33  		if err := parseGetParamsFromRequest(r, p); err != nil {
    34  			util.WriteErrResponse(w, http.StatusBadRequest, err)
    35  			return
    36  		}
    37  
    38  		p.Selector = "body"
    39  		if err := validateCSVRequest(r, p); err != nil {
    40  			util.WriteErrResponse(w, http.StatusBadRequest, err)
    41  			return
    42  		}
    43  
    44  		outBytes, err := inst.Dataset().GetCSV(r.Context(), p)
    45  		if err != nil {
    46  			util.RespondWithError(w, err)
    47  			return
    48  		}
    49  		publishDownloadEvent(r.Context(), inst, p.Ref)
    50  		writeFileResponse(w, outBytes, "body.csv", "csv")
    51  	}
    52  }
    53  
    54  // GetHandler is a dataset single endpoint
    55  func GetHandler(inst *lib.Instance, routePrefix string) http.HandlerFunc {
    56  	return func(w http.ResponseWriter, r *http.Request) {
    57  		if r.Method != http.MethodGet {
    58  			util.NotFoundHandler(w, r)
    59  			return
    60  		}
    61  		p := &lib.GetParams{}
    62  		if err := parseGetParamsFromRequest(r, p); err != nil {
    63  			util.WriteErrResponse(w, http.StatusBadRequest, err)
    64  			return
    65  		}
    66  
    67  		format := r.FormValue("format")
    68  
    69  		switch {
    70  		case format == "csv", arrayContains(r.Header["Accept"], "text/csv"):
    71  			// Examples:
    72  			// curl http://localhost:2503/ds/get/b5/world_bank_population/body?format=csv
    73  			// curl -H "Accept: text/csv" http://localhost:2503/ds/get/b5/world_bank_population/body
    74  			if err := validateCSVRequest(r, p); err != nil {
    75  				util.WriteErrResponse(w, http.StatusBadRequest, err)
    76  				return
    77  			}
    78  			outBytes, err := inst.Dataset().GetCSV(r.Context(), p)
    79  			if err != nil {
    80  				util.RespondWithError(w, err)
    81  				return
    82  			}
    83  
    84  			publishDownloadEvent(r.Context(), inst, p.Ref)
    85  			writeFileResponse(w, outBytes, "body.csv", "csv")
    86  			return
    87  
    88  		case format == "zip", arrayContains(r.Header["Accept"], "application/zip"):
    89  			// Examples:
    90  			// curl -H "Accept: application/zip" http://localhost:2503/ds/get/world_bank_population
    91  			// curl http://localhost:2503/ds/get/world_bank_population?format=zip
    92  			if err := validateZipRequest(r, p); err != nil {
    93  				util.WriteErrResponse(w, http.StatusBadRequest, err)
    94  				return
    95  			}
    96  			zipResults, err := inst.Dataset().GetZip(r.Context(), p)
    97  			if err != nil {
    98  				util.RespondWithError(w, err)
    99  				return
   100  			}
   101  			publishDownloadEvent(r.Context(), inst, p.Ref)
   102  			writeFileResponse(w, zipResults.Bytes, zipResults.GeneratedName, "zip")
   103  			return
   104  
   105  		default:
   106  			res, err := inst.Dataset().Get(r.Context(), p)
   107  			if err != nil {
   108  				util.RespondWithError(w, err)
   109  				return
   110  			}
   111  
   112  			if lib.IsSelectorScriptFile(p.Selector) {
   113  				util.WriteResponse(w, res.Bytes)
   114  				return
   115  			}
   116  
   117  			util.WriteResponse(w, res.Value)
   118  		}
   119  	}
   120  }
   121  
   122  func validateCSVRequest(r *http.Request, p *lib.GetParams) error {
   123  	format := r.FormValue("format")
   124  	if p.Selector != "body" {
   125  		return fmt.Errorf("can only get csv of the body component, selector must be 'body'")
   126  	}
   127  	if !(format == "csv" || format == "") {
   128  		return fmt.Errorf("format %q conflicts with requested body csv file", format)
   129  	}
   130  	return nil
   131  }
   132  
   133  func validateZipRequest(r *http.Request, p *lib.GetParams) error {
   134  	format := r.FormValue("format")
   135  	if p.Selector != "" {
   136  		return fmt.Errorf("can only get zip file of the entire dataset, got selector %q", p.Selector)
   137  	}
   138  	if !(format == "zip" || format == "") {
   139  		return fmt.Errorf("format %q conflicts with header %q", format, "Accept: application/zip")
   140  	}
   141  	return nil
   142  }
   143  
   144  // UnpackHandler unpacks a zip file and sends it back as json
   145  func UnpackHandler(routePrefix string) http.HandlerFunc {
   146  	return func(w http.ResponseWriter, r *http.Request) {
   147  		if r.Method != http.MethodPost {
   148  			util.NotFoundHandler(w, r)
   149  			return
   150  		}
   151  		postData, err := ioutil.ReadAll(r.Body)
   152  		if err != nil {
   153  			util.WriteErrResponse(w, http.StatusBadRequest, err)
   154  			return
   155  		}
   156  		contents, err := archive.UnzipGetContents(postData)
   157  		if err != nil {
   158  			util.WriteErrResponse(w, http.StatusInternalServerError, err)
   159  			return
   160  		}
   161  		data, err := json.Marshal(contents)
   162  		if err != nil {
   163  			util.WriteErrResponse(w, http.StatusInternalServerError, err)
   164  			return
   165  		}
   166  		util.WriteResponse(w, json.RawMessage(data))
   167  	}
   168  }
   169  
   170  // SaveByUploadHandler saves a dataset by reading the body from a file
   171  func SaveByUploadHandler(inst *lib.Instance, routePrefix string) http.HandlerFunc {
   172  	return func(w http.ResponseWriter, r *http.Request) {
   173  		if r.Method != http.MethodPost {
   174  			util.NotFoundHandler(w, r)
   175  			return
   176  		}
   177  
   178  		if err := r.ParseMultipartForm(maxBodyFileSize); err != nil {
   179  			util.WriteErrResponse(w, http.StatusBadRequest, err)
   180  			return
   181  		}
   182  
   183  		p := &lib.SaveParams{}
   184  		if err := parseSaveParamsFromRequest(r, p); err != nil {
   185  			util.WriteErrResponse(w, http.StatusBadRequest, err)
   186  			return
   187  		}
   188  
   189  		if p.Dataset == nil {
   190  			p.Dataset = &dataset.Dataset{}
   191  		}
   192  		if err := parseDatasetFromRequest(r, p.Dataset); err != nil {
   193  			util.WriteErrResponse(w, http.StatusBadRequest, err)
   194  			return
   195  		}
   196  
   197  		if p.Dataset.BodyFile() != nil {
   198  			// the `Save` method uses the `p.BodyPath` field to generate
   199  			// a default dataset name name if one is not given in the ref
   200  			p.BodyPath = p.Dataset.BodyFile().FileName()
   201  		}
   202  
   203  		ds, err := inst.Dataset().Save(r.Context(), p)
   204  		if err != nil {
   205  			util.RespondWithError(w, err)
   206  			return
   207  		}
   208  		util.WriteResponse(w, ds)
   209  	}
   210  }
   211  
   212  func extensionToMimeType(ext string) string {
   213  	switch ext {
   214  	case ".csv":
   215  		return "text/csv"
   216  	case ".json":
   217  		return "application/json"
   218  	case ".yaml":
   219  		return "application/x-yaml"
   220  	case ".xlsx":
   221  		return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
   222  	case ".zip":
   223  		return "application/zip"
   224  	case ".txt":
   225  		return "text/plain"
   226  	case ".md":
   227  		return "text/x-markdown"
   228  	case ".html":
   229  		return "text/html"
   230  	default:
   231  		return ""
   232  	}
   233  }
   234  
   235  func writeFileResponse(w http.ResponseWriter, val []byte, filename, format string) {
   236  	w.Header().Set("Content-Type", extensionToMimeType("."+format))
   237  	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
   238  	w.Write(val)
   239  }
   240  
   241  func arrayContains(subject []string, target string) bool {
   242  	for _, v := range subject {
   243  		if v == target {
   244  			return true
   245  		}
   246  	}
   247  	return false
   248  }
   249  
   250  func publishDownloadEvent(ctx context.Context, inst *lib.Instance, refStr string) {
   251  	ref, _, err := inst.ParseAndResolveRef(ctx, refStr, "local")
   252  	if err != nil {
   253  		log.Debugw("api.GetBodyCSVHandler - unable to resolve ref %q", err)
   254  		return
   255  	}
   256  	inst.Bus().Publish(ctx, event.ETDatasetDownload, ref.InitID)
   257  }