// sentNotModified will check the provided modified timestamp against
// either the X-If-Modified-Since or X-If-Unmodified-Since and return
// true if it wrote to w
func sentNotModified(w http.ResponseWriter, r *http.Request, modified int) (sentResponse bool) {
	ts, mHeaderType, err := extractModifiedTimestamp(r)
	if err != nil {
		sendRequestProblem(w, r, http.StatusBadRequest, err)
		return true
	}

	switch {
	case mHeaderType == X_IF_MODIFIED_SINCE && modified <= ts:
		w.Header().Set("X-Last-Modified", syncstorage.ModifiedToString(modified))
		sendRequestProblem(w, r, http.StatusNotModified, errors.New("Not Modified"))

		return true
	case mHeaderType == X_IF_UNMODIFIED_SINCE && modified > ts:
		w.Header().Set("X-Last-Modified", syncstorage.ModifiedToString(modified))
		sendRequestProblem(w, r, http.StatusPreconditionFailed,
			errors.Errorf("Condition requires %s, but modified at %s (△ %ss)",
				syncstorage.ModifiedToString(ts),
				syncstorage.ModifiedToString(modified),
				syncstorage.ModifiedToString(ts-modified)))

		return true
	}

	return false
}
func (s *SyncUserHandler) hCollectionDELETE(w http.ResponseWriter, r *http.Request) {

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

	if err != nil {
		if err == syncstorage.ErrNotFound {
			w.Header().Set("Content-Type", "application/json")
			fmt.Fprintf(w, `{"modified":%s}`, syncstorage.ModifiedToString(syncstorage.Now()))
			return
		} else {
			InternalError(w, r, err)
		}
		return
	}

	cmodified, err := s.db.GetCollectionModified(cId)
	if err != nil {
		InternalError(w, r, err)
		return
	} else if sentNotModified(w, r, cmodified) {
		return
	}

	modified := syncstorage.Now()
	bids, idExists := r.URL.Query()["ids"]
	if idExists {

		bidlist := strings.Split(bids[0], ",")

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

		modified, err = s.db.DeleteBSOs(cId, bidlist...)
		if err != nil {
			InternalError(w, r, err)
			return
		}
	} else {
		err = s.db.DeleteCollection(cId)
		if err != nil {
			InternalError(w, r, err)
			return
		}
	}

	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, `{"modified":%s}`, syncstorage.ModifiedToString(modified))
}
// hCollectionPOSTClassic is the historical POST handling logic prior to
// the addition of atomic commits from multiple POST requests
func (s *SyncUserHandler) hCollectionPOSTClassic(collectionId int, w http.ResponseWriter, r *http.Request) {

	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
	}

	if len(bsoToBeProcessed) > s.config.MaxPOSTRecords {
		sendRequestProblem(w, r, http.StatusRequestEntityTooLarge,
			errors.Errorf("Exceed %d BSO per request", s.config.MaxPOSTRecords))
		return
	}

	// Send the changes to the database and merge
	// with `results` above
	postResults, err := s.db.PostBSOs(collectionId, bsoToBeProcessed)

	if err != nil {
		InternalError(w, r, err)
	} else {
		for bsoId, failMessage := range postResults.Failed {
			results.Failed[bsoId] = failMessage
		}

		w.Header().Set("X-Last-Modified", syncstorage.ModifiedToString(postResults.Modified))
		JsonNewline(w, r, &PostResults{
			Modified: postResults.Modified,
			Success:  postResults.Success,
			Failed:   results.Failed,
		})
	}
}
func (s *SyncUserHandler) hInfoCollectionUsage(w http.ResponseWriter, r *http.Request) {
	if !AcceptHeaderOk(w, r) {
		return
	}

	modified, err := s.db.LastModified()
	if err != nil {
		InternalError(w, r, err)
		return
	}

	if sentNotModified(w, r, modified) {
		return
	}

	if results, err := s.db.InfoCollectionUsage(); err != nil {
		InternalError(w, r, err)
		return
	} else {
		// the sync 1.5 api says data should be in KB
		resultsKB := make(map[string]float64)
		for name, bytes := range results {
			resultsKB[name] = float64(bytes) / 1024
		}
		m := syncstorage.ModifiedToString(modified)
		w.Header().Set("X-Last-Modified", m)
		JsonNewline(w, r, resultsKB)
	}
}
// hInfoQuota calculates the total disk space used by the user by calculating
// it based on the number of DB pages used * size of each page.
// TODO actually implement quotas in the system.
func (s *SyncUserHandler) hInfoQuota(w http.ResponseWriter, r *http.Request) {
	results, err := s.db.InfoCollectionUsage()
	if err != nil {
		InternalError(w, r, err)
		return
	}

	modified, err := s.db.LastModified()
	if err != nil {
		InternalError(w, r, err)
		return
	}

	if sentNotModified(w, r, modified) {
		return
	}

	used := 0
	for _, bytes := range results {
		used += bytes
	}

	m := syncstorage.ModifiedToString(modified)
	w.Header().Set("X-Last-Modified", m)

	tmp := float64(used) / 1024
	JsonNewline(w, r, []*float64{&tmp, nil}) // crazy pointer cause need the nil
}
func (s *SyncUserHandler) hDeleteEverything(w http.ResponseWriter, r *http.Request) {
	err := s.db.DeleteEverything()
	if err != nil {
		InternalError(w, r, err)
	} else {
		m := syncstorage.ModifiedToString(syncstorage.Now())
		w.Header().Set("Content-Type", "text/plain")
		w.Header().Set("X-Last-Modified", m)
		w.Write([]byte(m))
	}
}
func (s *SyncUserHandler) hBsoGET(w http.ResponseWriter, r *http.Request) {

	if !AcceptHeaderOk(w, r) {
		return
	}

	var (
		bId string
		ok  bool
		cId int
		err error
		bso *syncstorage.BSO
	)

	if bId, ok = extractBsoIdFail(w, r); !ok {
		return
	}

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

	if err != nil {
		if err == syncstorage.ErrNotFound {
			sendRequestProblem(w, r, http.StatusNotFound, errors.Wrap(err, "Collection Not Found"))
		} else {
			InternalError(w, r, err)
		}
		return
	}

	if bso, err = s.db.GetBSO(cId, bId); err == nil {

		if sentNotModified(w, r, bso.Modified) {
			return
		}

		log.WithFields(log.Fields{
			"bso_t": bso.TTL,
			"now":   syncstorage.Now(),
			"diff":  syncstorage.Now() - bso.TTL,
		}).Debug("bso-expired")

		m := syncstorage.ModifiedToString(bso.Modified)
		w.Header().Set("X-Last-Modified", m)
		JsonNewline(w, r, bso)
	} else {
		if err == syncstorage.ErrNotFound {
			sendRequestProblem(w, r, http.StatusNotFound, errors.Wrap(err, "BSO Not Found"))
		} else {
			InternalError(w, r, err)
		}
	}
}
func (s *SyncUserHandler) hInfoCollections(w http.ResponseWriter, r *http.Request) {

	if !AcceptHeaderOk(w, r) {
		return
	}

	if info, err := s.db.InfoCollections(); err != nil {
		InternalError(w, r, err)
		return
	} else {
		modified := 0
		for _, modtime := range info {
			if modtime > modified {
				modified = modtime
			}
		}

		if sentNotModified(w, r, modified) {
			return
		}

		m := syncstorage.ModifiedToString(modified)
		w.Header().Set("X-Last-Modified", m)
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprint(w, "{")
		num := len(info)
		for name, modified := range info {
			fmt.Fprintf(w, `"%s":%s`, name, syncstorage.ModifiedToString(modified))
			num--
			if num != 0 {
				fmt.Fprint(w, ",")
			}
		}
		fmt.Fprint(w, "}")
	}
}
func (s *SyncUserHandler) hBsoDELETE(w http.ResponseWriter, r *http.Request) {
	var (
		bId      string
		ok       bool
		cId      int
		modified int
		err      error
	)

	if bId, ok = extractBsoIdFail(w, r); !ok {
		return
	}

	cId, err = s.getcid(r, false)
	if err == syncstorage.ErrNotFound {
		sendRequestProblem(w, r, http.StatusNotAcceptable, errors.Wrap(err, "Could not find collection"))
		return
	}

	// Trying to delete a BSO that is not there
	// should 404
	bso, err := s.db.GetBSO(cId, bId)
	if err != nil {
		if err == syncstorage.ErrNotFound {
			sendRequestProblem(w, r, http.StatusNotFound, errors.Errorf("BSO id: %s Not Found", bId))
		} else {
			InternalError(w, r, err)
		}
		return
	}

	if sentNotModified(w, r, bso.Modified) {
		return
	}

	modified, err = s.db.DeleteBSO(cId, bso.Id)

	if err != nil {
		InternalError(w, r, err)
	} else {
		m := syncstorage.ModifiedToString(modified)
		w.Header().Set("Content-Type", "text/plain")
		w.Header().Set("X-Last-Modified", m)
		w.Write([]byte(m))
	}
}
Пример #10
0
// MarshalJSON manually creates the JSON string since the modified needs to be
// converted in the python (ugh) timeformat required for sync 1.5. Which means no quotes
func (p *PostResults) MarshalJSON() ([]byte, error) {
	buf := new(bytes.Buffer)

	buf.WriteString(`{"modified":`)
	buf.WriteString(syncstorage.ModifiedToString(p.Modified))
	buf.WriteString(",")
	if len(p.Success) == 0 {
		buf.WriteString(`"success":[]`)
	} else {
		buf.WriteString(`"success":`)
		data, err := json.Marshal(p.Success)
		if err != nil {
			return nil, err
		}
		_, err = buf.Write(data)
		if err != nil {
			return nil, errors.Wrap(err, "Could not encode PostResults.Success")
		}
	}

	buf.WriteString(",")
	if len(p.Failed) == 0 {
		buf.WriteString(`"failed":{}`)
	} else {
		buf.WriteString(`"failed":`)
		data, err := json.Marshal(p.Failed)
		if err != nil {
			return nil, err
		}
		_, err = buf.Write(data)
		if err != nil {
			return nil, errors.Wrap(err, "Could not encode PostResults.Failed")
		}
	}

	if p.Batch != "" {
		buf.WriteString(`,"batch":"`)
		buf.WriteString(p.Batch)
		buf.WriteString(`"`)
	}

	buf.WriteString("}")
	return buf.Bytes(), nil
}
func (s *SyncUserHandler) hInfoCollectionCounts(w http.ResponseWriter, r *http.Request) {
	if !AcceptHeaderOk(w, r) {
		return
	}
	results, err := s.db.InfoCollectionCounts()
	if err != nil {
		InternalError(w, r, err)
		return
	}

	modified, err := s.db.LastModified()
	if err != nil {
		InternalError(w, r, err)
		return
	}

	if sentNotModified(w, r, modified) {
		return
	}

	m := syncstorage.ModifiedToString(modified)
	w.Header().Set("X-Last-Modified", m)
	JsonNewline(w, r, results)
}
// 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)
	}
}
func (s *SyncUserHandler) hBsoPUT(w http.ResponseWriter, r *http.Request) {
	if !AcceptHeaderOk(w, r) {
		return
	}

	// accept text/plain from old (broken) clients
	ct := getMediaType(r.Header.Get("Content-Type"))
	if ct != "application/json" && ct != "text/plain" && ct != "application/newlines" {
		sendRequestProblem(w, r, http.StatusUnsupportedMediaType, errors.Errorf("Not acceptable Content-Type: %s", ct))
		return
	}

	var (
		bId      string
		ok       bool
		cId      int
		modified int
		err      error
	)

	if bId, ok = extractBsoIdFail(w, r); !ok {
		return
	}

	cId, err = s.getcid(r, true)
	if err != nil {
		InternalError(w, r, err)
		return
	}

	modified, err = s.db.GetBSOModified(cId, bId)
	if err != nil {
		if err != syncstorage.ErrNotFound {
			InternalError(w, r, errors.Wrap(err, "Could not get Modified ts"))
			return
		}
	}

	if sentNotModified(w, r, modified) {
		return
	}

	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		InternalError(w, r, errors.New("PUT could not read JSON body"))
		return
	}

	var bso syncstorage.PutBSOInput
	if err := parseIntoBSO(body, &bso); err != nil {
		WeaveInvalidWBOError(w, r, errors.Wrap(err, "Could not parse body into BSO"))
		return
	}

	if bso.Payload != nil && len(*bso.Payload) > s.config.MaxRecordPayloadBytes {
		sendRequestProblem(w, r,
			http.StatusRequestEntityTooLarge,
			errors.New("Payload too big"))
		return
	}

	// change bso.TTL to milliseconds (what the db uses)
	// from seconds (what client's send)
	if bso.TTL != nil {
		tmp := *bso.TTL * 1000
		bso.TTL = &tmp
	}

	modified, err = s.db.PutBSO(cId, bId, bso.Payload, bso.SortIndex, bso.TTL)

	if err != nil {
		sendRequestProblem(w, r, http.StatusBadRequest, err)
		return
	}
	m := syncstorage.ModifiedToString(modified)
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("X-Last-Modified", m)
	w.Write([]byte(m))
}
	"strings"
	"testing"
	"time"

	"github.com/mozilla-services/go-syncstorage/syncstorage"
	"github.com/mozilla-services/go-syncstorage/token"
	"github.com/stretchr/testify/assert"
)

// generates a unique response each time so caching can be tested
var cacheMockHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {

	resp := struct {
		Type  string
		Value string
	}{"OK", syncstorage.ModifiedToString(syncstorage.Now())}

	w.Header().Set("X-Last-Modified", resp.Value)

	if infoCollectionsRoute.MatchString(req.URL.Path) {
		resp.Type = "info/collections"
		JSON(w, req, http.StatusOK, resp)
	} else if infoConfigurationRoute.MatchString(req.URL.Path) {
		resp.Type = "info/configuration"
		JSON(w, req, http.StatusOK, resp)
	} else {
		JSON(w, req, http.StatusOK, resp)
	}
})

func TestCacheHandlerInfoCollections(t *testing.T) {