Example #1
0
// extractBsoId tries to extract and validate a BSO id in the path
func extractBsoId(r *http.Request) (bId string, ok bool) {
	bId, ok = mux.Vars(r)["bsoId"]
	if !ok {
		return
	}

	ok = syncstorage.BSOIdOk(bId)
	return
}
// hCollectionPOSTBatch handles batch=? requests. It is called internally by hCollectionPOST
// to handle batch request logic
func (s *SyncUserHandler) hCollectionPOSTBatch(collectionId int, w http.ResponseWriter, r *http.Request) {

	// CHECK client provided headers to quickly determine if batch exceeds limits
	// this is meant to be a cheap(er) check without actually having to parse the
	// data provided by the user
	for _, headerName := range []string{"X-Weave-Total-Records", "X-Weave-Total-Bytes", "X-Weave-Records", "X-Weave-Bytes"} {
		if strVal := r.Header.Get(headerName); strVal != "" {
			if intVal, err := strconv.Atoi(strVal); err == nil {
				max := 0
				switch headerName {
				case "X-Weave-Total-Records":
					max = s.config.MaxTotalRecords
				case "X-Weave-Total-Bytes":
					max = s.config.MaxTotalBytes
				case "X-Weave-Bytes":
					max = s.config.MaxPOSTBytes
				case "X-Weave-Records":
					max = s.config.MaxPOSTRecords
				}

				if intVal > max {
					WeaveSizeLimitExceeded(w, r,
						errors.Errorf("Limit %s exceed. %d/%d", headerName, intVal, max))
					return
				}
			} else {
				// header value is invalid (not an int)
				sendRequestProblem(w, r, http.StatusBadRequest, errors.Errorf("Invalid integer value for %s", headerName))
				return
			}
		}
	}

	// CHECK the POST size, if possible from client supplied data
	// hopefully shortcut a fail if this exceeds limits
	if r.ContentLength > 0 && r.ContentLength > int64(s.config.MaxPOSTBytes) {
		WeaveSizeLimitExceeded(w, r,
			errors.Errorf("MaxPOSTBytes exceeded in request.ContentLength(%d) > %d",
				r.ContentLength, s.config.MaxPOSTBytes))
		return
	}

	// EXTRACT actual data to check
	bsoToBeProcessed, results, err := RequestToPostBSOInput(r, s.config.MaxRecordPayloadBytes)
	if err != nil {
		WeaveInvalidWBOError(w, r, errors.Wrap(err, "Failed turning POST body into BSO work list"))
		return
	}

	// CHECK actual BSOs sent to see if they exceed limits
	if len(bsoToBeProcessed) > s.config.MaxPOSTRecords {
		sendRequestProblem(w, r, http.StatusRequestEntityTooLarge,
			errors.Errorf("Exceeded %d BSO per request", s.config.MaxPOSTRecords))
		return
	}

	// CHECK BSO decoding validation errors. Don't even start a Batch if there are.
	if len(results.Failed) > 0 {
		modified := syncstorage.Now()
		w.Header().Set("X-Last-Modified", syncstorage.ModifiedToString(modified))
		JsonNewline(w, r, &PostResults{
			Modified: modified,
			Success:  nil,
			Failed:   results.Failed,
		})
		return
	}

	// Get batch id, commit command and internal collection Id
	_, batchId, batchCommit := GetBatchIdAndCommit(r)

	// CHECK batch id is valid for appends. Do this before loading and decoding
	// the body to be more efficient.
	if batchId != "true" {
		id, err := batchIdInt(batchId)
		if err != nil {
			sendRequestProblem(w, r, http.StatusBadRequest, errors.Wrap(err, "Invalid Batch ID Format"))
			return
		}

		if found, err := s.db.BatchExists(id, collectionId); err != nil {
			InternalError(w, r, err)
		} else if !found {
			sendRequestProblem(w, r, http.StatusBadRequest,
				errors.Errorf("Batch id: %s does not exist", batchId))
		}
	}

	filteredBSOs := make([]*syncstorage.PutBSOInput, 0, len(bsoToBeProcessed))
	failures := make(map[string][]string)

	for _, putInput := range bsoToBeProcessed {
		var failId string
		var failReason string

		if !syncstorage.BSOIdOk(putInput.Id) {
			failId = "na"
			failReason = fmt.Sprintf("Invalid BSO id %s", putInput.Id)
		}

		if putInput.SortIndex != nil && !syncstorage.SortIndexOk(*putInput.SortIndex) {
			failId = putInput.Id
			failReason = fmt.Sprintf("Invalid sort index for: %s", putInput.Id)
		}

		if putInput.TTL != nil && !syncstorage.TTLOk(*putInput.TTL) {
			failId = putInput.Id
			failReason = fmt.Sprintf("Invalid TTL for: %s", putInput.Id)
		}

		if failReason != "" {
			if failures[failId] == nil {
				failures[failId] = []string{failReason}
			} else {
				failures[failId] = append(failures[failId], failReason)
			}
			continue
		}

		filteredBSOs = append(filteredBSOs, putInput)
	}

	// JSON Serialize the data for storage in the DB
	buf := new(bytes.Buffer)
	if len(filteredBSOs) > 0 {
		encoder := json.NewEncoder(buf)
		for _, bso := range filteredBSOs {
			if err := encoder.Encode(bso); err != nil { // Note: this writes a newline after each record
				// whoa... presumably should never happen
				InternalError(w, r, errors.Wrap(err, "Failed encoding BSO for payload"))
				return
			}
		}
	}

	// Save either as a new batch or append to an existing batch
	var dbBatchId int
	//   - batchIdInt used to track the internal batchId number in the database after
	//   - the create || append

	appendedOkIds := make([]string, 0, len(filteredBSOs))
	if batchId == "true" {
		newBatchId, err := s.db.BatchCreate(collectionId, buf.String())
		if err != nil {
			InternalError(w, r, errors.Wrap(err, "Failed creating batch"))
			return
		}

		dbBatchId = newBatchId
	} else {
		id, err := batchIdInt(batchId)
		if err != nil {
			sendRequestProblem(w, r, http.StatusBadRequest, errors.Wrap(err, "Invalid Batch ID Format"))
			return
		}

		if len(filteredBSOs) > 0 { // append only if something to do
			if err := s.db.BatchAppend(id, collectionId, buf.String()); err != nil {
				InternalError(w, r, errors.Wrap(err, fmt.Sprintf("Failed append to batch id:%d", dbBatchId)))
				return
			}

		}

		dbBatchId = id
	}

	// The success list only includes the BSO Ids in the
	// POST request. These need to kept to be sent back in
	// the response. Matches the python server's behaviour / passes
	// the tests
	for _, bso := range filteredBSOs {
		appendedOkIds = append(appendedOkIds, bso.Id)
	}

	if batchCommit {
		batchRecord, err := s.db.BatchLoad(dbBatchId, collectionId)
		if err != nil {
			InternalError(w, r, errors.Wrap(err, "Failed Loading Batch to commit"))
			return
		}

		rawJSON := ReadNewlineJSON(bytes.NewBufferString(batchRecord.BSOS))

		// CHECK final data before committing it to the database
		numInBatch := len(rawJSON)
		if numInBatch > s.config.MaxTotalRecords {
			s.db.BatchRemove(dbBatchId)
			WeaveSizeLimitExceeded(w, r,
				errors.Errorf("Too many BSOs (%d) in Batch(%d)", numInBatch, dbBatchId))
			return
		}

		postData := make(syncstorage.PostBSOInput, len(rawJSON), len(rawJSON))
		for i, bsoJSON := range rawJSON {
			var bso syncstorage.PutBSOInput
			if parseErr := parseIntoBSO(bsoJSON, &bso); parseErr != nil {
				// well there is definitely a bug somewhere if this happens
				InternalError(w, r, errors.Wrap(parseErr, "Could not decode batch data"))
				return
			} else {
				postData[i] = &bso
			}
		}

		// CHECK that actual Batch data size
		sum := 0
		for _, bso := range postData {
			if bso.Payload == nil {
				continue
			}

			sum := sum + len(*bso.Payload)
			if sum > s.config.MaxTotalBytes {
				s.db.BatchRemove(dbBatchId)
				WeaveSizeLimitExceeded(w, r,
					errors.Errorf("Batch size(%d) exceeded MaxTotalBytes limit(%d)",
						sum, s.config.MaxTotalBytes))

				return
			}
		}

		postResults, err := s.db.PostBSOs(collectionId, postData)
		if err != nil {
			InternalError(w, r, err)
			return
		}

		// merge failures
		for key, reasons := range postResults.Failed {
			if failures[key] == nil {
				failures[key] = reasons
			} else {
				failures[key] = append(failures[key], reasons...)
			}
		}

		// DELETE the batch from the DB
		s.db.BatchRemove(dbBatchId)

		w.Header().Set("X-Last-Modified", syncstorage.ModifiedToString(postResults.Modified))

		JsonNewline(w, r, &PostResults{
			Modified: postResults.Modified,
			Success:  appendedOkIds,
			Failed:   failures,
		})
	} else {
		modified := syncstorage.Now()
		w.Header().Set("X-Last-Modified", syncstorage.ModifiedToString(modified))
		JsonNewlineStatus(w, r, http.StatusAccepted, &PostResults{
			Batch:    batchIdString(dbBatchId),
			Modified: modified,
			Success:  appendedOkIds,
			Failed:   failures,
		})
	}
}
func (s *SyncUserHandler) hCollectionGET(w http.ResponseWriter, r *http.Request) {

	if !AcceptHeaderOk(w, r) {
		return
	}

	// query params that control searching
	var (
		err    error
		ids    []string
		newer  int
		older  int
		full   bool
		limit  int
		offset int
		sort   = syncstorage.SORT_NEWEST
	)

	cId, err := s.getcid(r, false)

	if err != nil {
		if err == syncstorage.ErrNotFound {
			w.Header().Set("Content-Type", "application/json")
			w.Write([]byte("[]"))
			return
		} else {
			InternalError(w, r, err)
			return
		}
	}

	if err = r.ParseForm(); err != nil {
		sendRequestProblem(w, r, http.StatusBadRequest, errors.Wrap(err, "Bad query parameters"))
		return
	}

	if v := r.Form.Get("ids"); v != "" {
		ids = strings.Split(v, ",")

		if len(ids) > s.config.MaxPOSTRecords {
			sendRequestProblem(w, r, http.StatusBadRequest, errors.New("Exceeded max batch size"))
			return
		}

		for i, id := range ids {
			id = strings.TrimSpace(id)
			if syncstorage.BSOIdOk(id) {
				ids[i] = id
			} else {
				sendRequestProblem(w, r, http.StatusBadRequest, errors.Errorf("Invalid bso id %s", id))
				return
			}
		}

		if len(ids) > 100 {
			sendRequestProblem(w, r, http.StatusBadRequest, errors.New("Too many ids provided"))
			return
		}
	}

	// we expect to get sync's two decimal timestamps, these need
	// to be converted to milliseconds
	if v := r.Form.Get("older"); v != "" {
		floatNew, err := strconv.ParseFloat(v, 64)
		if err != nil {
			sendRequestProblem(w, r, http.StatusBadRequest, errors.Wrap(err, "Invalid older param format"))
			return
		}

		older = int(floatNew * 1000)
		if !syncstorage.NewerOk(newer) {
			sendRequestProblem(w, r, http.StatusBadRequest, errors.New("Invalid older value"))
			return
		}
	} else {
		older = syncstorage.MaxTimestamp
	}

	if v := r.Form.Get("newer"); v != "" {
		floatNew, err := strconv.ParseFloat(v, 64)
		if err != nil {
			sendRequestProblem(w, r, http.StatusBadRequest, errors.Wrap(err, "Invalid newer param format"))
			return
		}

		newer = int(floatNew * 1000)
		if !syncstorage.NewerOk(newer) {
			sendRequestProblem(w, r, http.StatusBadRequest, errors.New("Invalid newer value"))
			return
		}
	}

	if v := r.Form.Get("full"); v != "" {
		full = true
	}

	if v := r.Form.Get("limit"); v != "" {
		limit, err = strconv.Atoi(v)
		if err != nil || !syncstorage.LimitOk(limit) {
			errMessage := "Invalid limit value"
			if err != nil {
				err = errors.Wrap(err, errMessage)
			} else {
				err = errors.New(errMessage)
			}
			sendRequestProblem(w, r, http.StatusBadRequest, err)
			return
		}
	}

	// assign a default value for limit if nothing is supplied
	if limit <= 0 || limit > s.config.MaxBSOGetLimit {
		limit = s.config.MaxBSOGetLimit
	}

	if v := r.Form.Get("offset"); v != "" {
		offset, err = strconv.Atoi(v)
		if err != nil || !syncstorage.OffsetOk(offset) {
			errMessage := "Invalid offset value"
			if err != nil {
				err = errors.Wrap(err, errMessage)
			} else {
				err = errors.New(errMessage)
			}
			sendRequestProblem(w, r, http.StatusBadRequest, err)
			return
		}
	}

	if v := r.Form.Get("sort"); v != "" {
		switch v {
		case "newest":
			sort = syncstorage.SORT_NEWEST
		case "oldest":
			sort = syncstorage.SORT_OLDEST
		case "index":
			sort = syncstorage.SORT_INDEX
		default:
			sendRequestProblem(w, r, http.StatusBadRequest, errors.New("Invalid sort value"))
			return
		}
	}

	// this is way down here since IO is more expensive
	// than parsing if the GET params are valid
	cmodified, err := s.db.GetCollectionModified(cId)
	if err != nil {
		InternalError(w, r, err)
		return
	} else if sentNotModified(w, r, cmodified) {
		return
	}

	results, err := s.db.GetBSOs(cId, ids, older, newer, sort, limit, offset)
	if err != nil {
		InternalError(w, r, err)
		return
	}
	m := syncstorage.ModifiedToString(cmodified)
	w.Header().Set("X-Last-Modified", m)

	w.Header().Set("X-Weave-Records", strconv.Itoa(results.Total))
	if results.More {
		w.Header().Set("X-Weave-Next-Offset", strconv.Itoa(results.Offset))
	}

	if full {
		JsonNewline(w, r, results.BSOs)
	} else {
		bsoIds := make([]string, len(results.BSOs))
		for i, b := range results.BSOs {
			bsoIds[i] = b.Id
		}
		JsonNewline(w, r, bsoIds)
	}
}