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 }