uploadhandler.go (157 lines of code) (raw):

package main import ( "github.com/go-redis/redis/v8" "github.com/google/uuid" "gitlab.com/codmill/customer-projects/guardian/deliverable-receiver/helpers" "gitlab.com/codmill/customer-projects/guardian/deliverable-receiver/models" "io" "io/ioutil" "log" "net/http" "os" "path" "regexp" ) type UploadHandler struct { redisClient *redis.Client config *helpers.Config } /** sanitises the given incoming filename and combines it with the base path from config and the slot path */ func getTargetFilename(configBase string, slotBase string, requestedFilename string) string { fileBase := path.Base(requestedFilename) sanitizer := regexp.MustCompile("[^\\w\\d\\s.]") sanitizedFileBase := sanitizer.ReplaceAllString(fileBase, "") return path.Join(configBase, slotBase, sanitizedFileBase) } /** writes the data from the given reader out to the given filename. creates the parent directory if necessary */ func writeOutData(fullpath string, maybeRange *helpers.RangeHeader, content io.Reader) (int64, error) { dirpath := path.Dir(fullpath) if _, err := os.Stat(dirpath); os.IsNotExist(err) { log.Printf("INFO Uploadandler.writeOutData target path %s does not exist, creating", dirpath) mkDirErr := os.MkdirAll(dirpath, 0775) if mkDirErr != nil { log.Printf("ERROR UploadHandler.writeOutData could not create directory %s: %s", dirpath, mkDirErr) return -1, mkDirErr } } //FIXME: this over-writes an existing file openFlags := os.O_WRONLY if maybeRange != nil && maybeRange.IsFirst() { openFlags |= os.O_CREATE | os.O_TRUNC } f, openErr := os.OpenFile(fullpath, openFlags, 0664) if openErr != nil { log.Printf("ERROR UploadHandler.writeOutData could not open %s to write: %s", fullpath, openErr) return -1, openErr } defer f.Close() if maybeRange == nil || maybeRange.IsComplete() { log.Printf("INFO UploadHandler.writeOutData no range so writing whole file") return io.Copy(f, content) } else { _, seekErr := f.Seek(maybeRange.Start, os.SEEK_SET) if seekErr != nil { log.Printf("ERROR UploadHandler.writeOutData could not seek '%s': %s", fullpath, seekErr) return -1, seekErr } //FIXME: check if this actually writes correctly return io.Copy(f, content) } } /** perform an actual file upload. expects a POST request with two query parameters: - fileName: filename with no path. If any path parts are present, they are stripped. - uploadId: uuid of an existing upload created by calling /initiate. If it does not exist (any more?) a 404 is returned; the client should try to create a new one and then retry the upload. */ func (h UploadHandler) ServeHTTP(w http.ResponseWriter, request *http.Request) { if request.Body != nil { defer request.Body.Close() } if !helpers.AssertHttpMethod(request, w, "POST") { io.Copy(ioutil.Discard, request.Body) //discard any remaining body return } username, validationErr := helpers.ValidateLogin(request, h.config) if validationErr != nil { log.Printf("ERROR UploadHandler could not validate request: %s", validationErr) response := helpers.GenericErrorResponse{ Status: "forbidden", Detail: validationErr.Error(), } helpers.WriteJsonContent(response, w, 403) return } values, queryErr := helpers.GetQueryParams(request.RequestURI) if queryErr != nil { log.Print("ERROR UploadHandler could not parse own url: ", queryErr) response := helpers.GenericErrorResponse{ Status: "server_error", Detail: "invalid url", } helpers.WriteJsonContent(response, w, 400) return } uploadId := values.Get("uploadId") fileName := values.Get("fileName") if uploadId == "" { response := helpers.GenericErrorResponse{ Status: "invalid_request", Detail: "you must specify the uploadId query parameter", } helpers.WriteJsonContent(response, w, 400) return } uploadSlotUuid, uuidErr := uuid.Parse(uploadId) if uuidErr != nil { response := helpers.GenericErrorResponse{ Status: "invalid_request", Detail: "uploadId must be a uuid", } helpers.WriteJsonContent(response, w, 400) return } if fileName == "" { response := helpers.GenericErrorResponse{ Status: "invalid_request", Detail: "you must specify the fileName query parameter", } helpers.WriteJsonContent(response, w, 400) return } maybeRangeHeader, rangeErr := helpers.ExtractRange(request) if rangeErr != nil { log.Printf("ERROR UploadHandler could not parse range parameter '%s': %s", request.Header.Get("Range"), rangeErr) response := helpers.GenericErrorResponse{ Status: "invalid_request", Detail: rangeErr.Error(), } helpers.WriteJsonContent(response, w, 400) return } uploadSlot, slotErr := models.UploadSlotForId(uploadSlotUuid, h.redisClient) if slotErr != nil { log.Printf("ERROR UploadHandler could not get upload slot for '%s': %s", uploadSlotUuid, slotErr) response := helpers.GenericErrorResponse{ Status: "db_error", Detail: "could not get upload slot", } helpers.WriteJsonContent(response, w, 500) return } if uploadSlot == nil { log.Printf("WARNING UploadHandler no upload slot for '%s', this might just mean it's expire", uploadSlotUuid) response := helpers.GenericErrorResponse{ Status: "not_found", Detail: "upload slot does not exist", } helpers.WriteJsonContent(response, w, 404) return } //get a sanitised, absolute path to write the file targetFilename := getTargetFilename(h.config.StoragePrefix.LocalPath, uploadSlot.UploadPathRelative, fileName) log.Printf("INFO UploadHandler upload request from %s to %s", username, targetFilename) bytesWritten, writeErr := writeOutData(targetFilename, maybeRangeHeader, request.Body) if writeErr != nil { response := helpers.GenericErrorResponse{ Status: "write_error", Detail: writeErr.Error(), } helpers.WriteJsonContent(response, w, 500) return } response := map[string]interface{}{ "status": "ok", "bytes_written": bytesWritten, } helpers.WriteJsonContent(response, w, 200) }