Example #1
0
// this method only exists for compatibility with PouchDB and should not be used elsewhere.
func BulkGet(w http.ResponseWriter, r *http.Request) {
	ctx := getContext(r)
	var ireqs interface{}

	/* options */
	revs := flag(r, "revs")

	var ok bool
	if ireqs, ok = ctx.jsonBody["docs"]; !ok {
		res := responses.BadRequest("You were supposed to request some docs specified by their ids, and you didn't.")
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	reqs := ireqs.([]interface{})
	res := responses.BulkGet{
		Results: make([]responses.BulkGetResult, len(reqs)),
	}
	for i, ireq := range reqs {
		req := ireq.(map[string]interface{})
		res.Results[i] = responses.BulkGetResult{
			Docs: make([]responses.DocOrError, 1),
		}

		iid, ok := req["id"]
		if !ok {
			err := responses.BadRequest("missing id")
			res.Results[i].Docs[0].Error = &err
			continue
		}
		id := iid.(string)
		res.Results[i].Id = id

		path := db.CleanPath(ctx.path) + "/" + db.EscapeKey(id)
		rev := string(db.GetRev(path))
		doc, err := db.GetTreeAt(path)
		if err != nil {
			err := responses.NotFound()
			res.Results[i].Docs[0].Error = &err
			continue
		}

		doc["_id"] = id
		doc["_rev"] = rev
		delete(doc, "_val") // pouchdb doesn't like top-level keys starting with _

		if revs {
			// magic method to fetch _revisions
			// docs["_revisions"] = ...
		}

		res.Results[i].Docs[0].Ok = &doc
	}

	w.Header().Add("Content-Type", "application/json")
	w.WriteHeader(200)
	json.NewEncoder(w).Encode(res)
}
Example #2
0
func Post(w http.ResponseWriter, r *http.Request) {
	ctx := getContext(r)
	var err error
	var rev string

	if ctx.jsonBody == nil {
		res := responses.BadRequest("You must send a JSON body for this request.")
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	if ctx.lastKey == "_bulk_get" {
		BulkGet(w, r)
		return
	} else if ctx.lastKey == "_bulk_docs" {
		BulkDocs(w, r)
		return
	} else if ctx.lastKey == "_revs_diff" {
		RevsDiff(w, r)
		return
	}

	if ctx.lastKey[0] == '_' {
		res := responses.BadRequest("you can't post to special keys.")
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	// POST is supposed to generate a new doc, with a new random _id:
	id := db.Random(7)
	path := ctx.path + "/" + id

	if ctx.jsonBody != nil {
		// if it is JSON we must save it as its structure demands
		rev, err = db.SaveTreeAt(path, ctx.jsonBody)
	} else {
		// otherwise it's an error
		res := responses.BadRequest("you need to send a JSON body for this request.")
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	if err != nil {
		log.Error("couldn't save value: ", err)
		res := responses.UnknownError(err.Error())
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	w.WriteHeader(201)
	w.Header().Add("Content-Type", "application/json")
	json.NewEncoder(w).Encode(responses.Success{true, id, rev})
}
Example #3
0
func CreateUser(w http.ResponseWriter, r *http.Request) {
	ctx := getContext(r)

	if name, ok := ctx.jsonBody["name"]; ok {
		if password, ok := ctx.jsonBody["password"]; ok {
			err := db.SaveUser(name.(string), password.(string))
			if err != nil {
				res := responses.UnknownError(err.Error())
				w.WriteHeader(res.Code)
				json.NewEncoder(w).Encode(res)
				return
			} else {
				res := responses.Success{Ok: true}
				w.Header().Add("Content-Type", "application/json")
				w.WriteHeader(201)
				json.NewEncoder(w).Encode(res)
				return
			}
		}
	}

	res := responses.BadRequest(`You must send a JSON body with "name" and "password".`)
	w.WriteHeader(res.Code)
	json.NewEncoder(w).Encode(res)
}
Example #4
0
/* requests with raw string bodies:
   - curl -X PUT http://db/path/to/key/_val -d 'some value'
 and complete JSON objects
   - curl -X PUT http://db/path -d '{"to": {"_val": "nothing here", "key": "some value"}}' -H 'content-type: application/json'

While setting the raw string body of a path will only update that path and do not change others, a full JSON request will replace all keys under the specified path. PUT is idempotent.

The "_val" key is optional when setting, but can be used to set values right to the key to which they refer. It is sometimes needed, like in this example, here "path/to" had some children values to be set, but also needed a value of its own.
Other use of the "_val" key is to set a value to null strictly, because setting the nude key to null will delete it instead of setting it.
*/
func Put(w http.ResponseWriter, r *http.Request) {
	ctx := getContext(r)

	if ctx.lastKey == "_security" {
		WriteSecurity(w, r)
		return
	}

	var err error
	var rev string

	if ctx.exists && ctx.currentRev != ctx.providedRev {
		log.WithFields(log.Fields{
			"given":   ctx.providedRev,
			"current": ctx.currentRev,
		}).Debug("rev mismatch.")
		res := responses.ConflictError()
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	if ctx.lastKey == "_val" || ctx.jsonBody == nil {
		// updating the value at only this specific path, as raw bytes
		rev, err = db.SaveValueAt(ctx.path, db.ToLevel(ctx.body))
	} else if ctx.jsonBody != nil {
		// saving a JSON tree, as its structure demands, expanding to subpaths and so on
		if ctx.lastKey[0] == '_' {
			log.WithFields(log.Fields{
				"path":    ctx.path,
				"lastkey": ctx.lastKey,
			}).Debug("couldn't update special key.")
			res := responses.BadRequest("you can't update special keys")
			w.WriteHeader(res.Code)
			json.NewEncoder(w).Encode(res)
			return
		}

		if ctx.localDoc {
			rev, err = db.SaveLocalDocAt(ctx.path, ctx.jsonBody)
		} else {
			rev, err = db.ReplaceTreeAt(ctx.path, ctx.jsonBody, false)
		}
	} else {
		err = errors.New("value received at " + ctx.path + " is not JSON: " + string(ctx.body))
	}

	if err != nil {
		log.Debug("couldn't save value: ", err)
		res := responses.UnknownError(err.Error())
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	w.WriteHeader(201)
	w.Header().Add("Content-Type", "application/json")
	json.NewEncoder(w).Encode(responses.Success{true, ctx.lastKey, rev})
}
Example #5
0
/*
   should accept PATCH requests with JSON objects:
   `curl -X PATCH http://db/path -d '{"to": {"key": "some value"}}' -H 'content-type: application/json'`
   this will not replace all values under /path, but only modify the values which the JSON object refers to.
*/
func Patch(w http.ResponseWriter, r *http.Request) {
	ctx := getContext(r)
	var err error
	var rev string

	if ctx.lastKey[0] == '_' {
		res := responses.BadRequest("you can't update special keys")
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	if !ctx.exists {
		res := responses.NotFound()
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	if ctx.currentRev != ctx.providedRev {
		log.WithFields(log.Fields{
			"given":   ctx.providedRev,
			"current": ctx.currentRev,
		}).Debug("rev mismatch.")
		res := responses.ConflictError()
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	// update the tree as the JSON structure demands
	rev, err = db.SaveTreeAt(ctx.path, ctx.jsonBody)

	if err != nil {
		log.Debug("couldn't save value: ", err)
		res := responses.UnknownError(err.Error())
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	w.WriteHeader(200)
	w.Header().Add("Content-Type", "application/json")
	json.NewEncoder(w).Encode(responses.Success{true, ctx.lastKey, rev})
}
Example #6
0
func Delete(w http.ResponseWriter, r *http.Request) {
	ctx := getContext(r)
	var err error
	var rev string

	if !ctx.exists {
		res := responses.NotFound()
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	if ctx.lastKey[0] == '_' {
		res := responses.BadRequest("you can't delete special keys")
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	if ctx.currentRev != "" && ctx.currentRev != ctx.providedRev {
		log.WithFields(log.Fields{
			"given":   ctx.providedRev,
			"current": ctx.currentRev,
		}).Debug("rev mismatch.")
		res := responses.ConflictError()
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	rev, err = db.DeleteAt(ctx.path)
	if err != nil {
		log.Error("couldn't delete key at ", ctx.path, ": ", err)
		res := responses.NotFound()
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	w.WriteHeader(200)
	w.Header().Add("Content-Type", "application/json")
	json.NewEncoder(w).Encode(responses.Success{true, ctx.lastKey, rev})
}
Example #7
0
/*
   Currently _all_docs does not guarantee key order and should not be used for
   querying or anything. It is here just to provide PouchDB replication support.
*/
func AllDocs(w http.ResponseWriter, r *http.Request) {
	ctx := getContext(r)

	/* options */
	include_docs := flag(r, "include_docs")
	jsonKeys := param(r, "keys")
	// startkey := param(r, "startkey")
	// endkey := param(r, "endkey")
	// descending := flag(r, "descending")
	// skip := param(r, "skip")
	// limit := param(r, "limit")

	path := db.CleanPath(ctx.path)
	tree, err := db.GetTreeAt(path)
	if err != nil {
		res := responses.UnknownError(err.Error())
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	res := responses.AllDocs{
		TotalRows: 0,
		Offset:    0, // temporary
		Rows:      make([]responses.Row, 0),
	}

	if jsonKeys != "" {
		// in case "keys" was provided we can just pick the requested keys from our tree
		var keys []string
		err = json.Unmarshal([]byte(jsonKeys), &keys)
		if err != nil {
			res := responses.BadRequest()
			w.WriteHeader(res.Code)
			json.NewEncoder(w).Encode(res)
			return
		}

		for _, id := range keys {
			var doc interface{}
			var ok bool
			if doc, ok = tree[id]; !ok || strings.HasPrefix(id, "_") {
				res.Rows = append(res.Rows, responses.Row{
					Key:   id,
					Error: (responses.NotFound()).Reason,
				})
			}
			rev := string(db.GetRev(path + "/" + db.EscapeKey(id)))
			row := responses.Row{
				Id:    id,
				Key:   id,
				Value: map[string]interface{}{"rev": rev},
			}
			if include_docs {
				row.Doc = doc.(map[string]interface{})
				row.Doc["_id"] = id
				row.Doc["_rev"] = rev
				delete(row.Doc, "_val") // pouchdb doesn't like top-level keys starting with _
			}
			res.Rows = append(res.Rows, row)
			res.TotalRows += 1
		}
	} else {
		// otherwise iterate through the entire tree
		for id, doc := range tree {
			if strings.HasPrefix(id, "_") {
				continue
			}

			rev := string(db.GetRev(path + "/" + db.EscapeKey(id)))
			row := responses.Row{
				Id:    id,
				Key:   id,
				Value: map[string]interface{}{"rev": rev},
			}
			if include_docs {
				row.Doc = doc.(map[string]interface{})
				row.Doc["_id"] = id
				row.Doc["_rev"] = rev
				delete(row.Doc, "_val") // pouchdb doesn't like top-level keys starting with _
			}
			res.Rows = append(res.Rows, row)
			res.TotalRows += 1
		}
	}

	w.Header().Add("Content-Type", "application/json")
	w.WriteHeader(200)
	json.NewEncoder(w).Encode(res)
}
Example #8
0
// this method only exists for compatibility with PouchDB and should not be used elsewhere.
func BulkDocs(w http.ResponseWriter, r *http.Request) {
	ctx := getContext(r)
	var idocs interface{}
	var iid interface{}
	var irev interface{}
	var id string
	var rev string
	var ok bool

	if idocs, ok = ctx.jsonBody["docs"]; !ok {
		res := responses.BadRequest("You're supposed to send an array of docs to input on the database, and you didn't.")
		w.WriteHeader(res.Code)
		json.NewEncoder(w).Encode(res)
		return
	}

	/* options */
	new_edits := true
	if inew_edits, ok := ctx.jsonBody["new_edits"]; ok {
		new_edits = inew_edits.(bool)
	}

	path := db.CleanPath(ctx.path)
	docs := idocs.([]interface{})
	res := make([]responses.BulkDocsResult, len(docs))

	var err error
	if new_edits { /* procedure for normal document saving */
		for i, idoc := range docs {

			var newrev string
			localDoc := false
			doc := idoc.(map[string]interface{})
			if iid, ok = doc["_id"]; ok {
				id = iid.(string)
				if strings.HasPrefix(id, "_local/") {
					localDoc = true
				}
				if irev, ok = doc["_rev"]; ok {
					rev = irev.(string)
				} else {
					rev = ""
				}
			} else {
				id = db.Random(5)
			}

			if !localDoc {
				// procedure for normal documents

				// check rev matching:
				if iid != nil /* when iid is nil that means the doc had no _id, so we don't have to check. */ {
					currentRev := db.GetRev(path + "/" + db.EscapeKey(id))
					if string(currentRev) != rev && !bytes.HasPrefix(currentRev, []byte{'0', '-'}) {
						// rev is not matching, don't input this document
						e := responses.ConflictError()
						res[i] = responses.BulkDocsResult{
							Id:     id,
							Error:  e.Error,
							Reason: e.Reason,
						}
						continue
					}
				}

				// proceed to write the document.
				newrev, err = db.ReplaceTreeAt(path+"/"+db.EscapeKey(id), doc, false)
				if err != nil {
					e := responses.UnknownError()
					res[i] = responses.BulkDocsResult{
						Id:     id,
						Error:  e.Error,
						Reason: e.Reason,
					}
					continue
				}

			} else {
				// procedure for local documents

				// check rev matching:
				currentRev := db.GetLocalDocRev(path + "/" + id)
				if currentRev != "0-0" && currentRev != rev {
					// rev is not matching, don't input this document
					e := responses.ConflictError()
					res[i] = responses.BulkDocsResult{
						Id:     id,
						Error:  e.Error,
						Reason: e.Reason,
					}
					continue
				}

				// proceed to write the document.
				newrev, err = db.SaveLocalDocAt(path+"/"+id, doc)
				if err != nil {
					e := responses.UnknownError()
					res[i] = responses.BulkDocsResult{
						Id:     id,
						Error:  e.Error,
						Reason: e.Reason,
					}
					continue
				}
			}

			res[i] = responses.BulkDocsResult{
				Id:  id,
				Ok:  true,
				Rev: newrev,
			}
		}
	} else { /* procedure for replication-type _bulk_docs (new_edits=false)*/
		for i, idoc := range docs {
			id = ""
			rev = ""
			doc := idoc.(map[string]interface{})
			if iid, ok = doc["_id"]; ok {
				id = iid.(string)
				if irev, ok = doc["_rev"]; ok {
					rev = irev.(string)
				}
			}
			if id == "" || rev == "" {
				e := responses.UnknownError()
				res[i] = responses.BulkDocsResult{
					Id:     id,
					Error:  e.Error,
					Reason: e.Reason,
				}
				continue
			}

			// proceed to write
			currentRev := db.GetRev(path + "/" + db.EscapeKey(id))
			if rev < string(currentRev) {
				// will write only the rev if it is not the winning rev
				db.AcknowledgeRevFor(path+"/"+db.EscapeKey(id), rev)

			} else {
				// otherwise will write the whole doc normally (replacing the current)
				rev, err = db.ReplaceTreeAt(path+"/"+db.EscapeKey(id), doc, true)
			}

			// handle write error
			if err != nil {
				e := responses.UnknownError()
				res[i] = responses.BulkDocsResult{
					Id:     id,
					Error:  e.Error,
					Reason: e.Reason,
				}
				continue
			}

			res[i] = responses.BulkDocsResult{
				Id:  id,
				Ok:  true,
				Rev: rev,
			}
		}
	}

	w.WriteHeader(201)
	json.NewEncoder(w).Encode(res)
}
Example #9
0
func setCommonVariables(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := getContext(r)

		/* when the path is in the format /nanana/nanana/_val
		   we reply with the single value for that path, otherwise
		   assume the whole tree is being requested. */
		ctx.path = r.URL.Path
		ctx.wantsTree = true
		ctx.localDoc = false
		ctx.wantsDatabaseInfo = false

		entityPath := ctx.path
		pathKeys := db.SplitKeys(ctx.path)
		nkeys := len(pathKeys)
		ctx.lastKey = pathKeys[nkeys-1]

		if nkeys > 1 && pathKeys[nkeys-2] == "_local" {
			// workaround for couchdb-like _local/blablabla docs
			// we use a different sublevel for local docs so we must
			// acknowledge that somehow.
			ctx.lastKey = db.UnescapeKey(db.JoinKeys(pathKeys[nkeys-2:]))
			ctx.localDoc = true
		} else if ctx.lastKey == "" {
			// this means the request was made with an ending slash (for example:
			// https://my.summadb.com/path/to/here/), so it wants couchdb-like information
			// for the referred sub-database, and not the document at the referred path.
			// to get information on the document at the referred path the request must be
			// made without the trailing slash (or with a special header, for the root document
			// and other scenarios in which the user does not have control over the presence
			// of the ending slash).
			ctx.wantsDatabaseInfo = true

			if ctx.path == "/" {
				ctx.path = ""
				ctx.lastKey = ""
				entityPath = ""
			}
		} else {
			if ctx.lastKey[0] == '_' {
				ctx.wantsTree = false
				entityPath = db.JoinKeys(pathKeys[:nkeys-1])
				if ctx.lastKey == "_val" {
					ctx.path = db.JoinKeys(pathKeys[:nkeys-1])
					ctx.lastKey = pathKeys[nkeys-1]
				}
			}
		}

		ctx.currentRev = ""
		ctx.deleted = false
		qrev := r.URL.Query().Get("rev")
		hrev := r.Header.Get("If-Match")
		drev := ""
		ctx.providedRev = hrev // will be "" if there's no header rev

		// fetching current rev
		if !ctx.localDoc {
			// procedure for normal documents

			specialKeys, err := db.GetSpecialKeysAt(entityPath)
			if err == nil {
				ctx.currentRev = specialKeys.Rev
				ctx.deleted = specialKeys.Deleted
			}

		} else {
			// procedure for local documents
			ctx.currentRev = db.GetLocalDocRev(ctx.path)
		}

		// body parsing
		if r.Method[0] == 'P' { // PUT, PATCH, POST
			/* filter body size */
			b, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
			ctx.body = b
			if err != nil {
				res := responses.BadRequest("request body too large")
				w.WriteHeader(res.Code)
				json.NewEncoder(w).Encode(res)
				return
			}

			if ctx.lastKey != "_val" {
				err = json.Unmarshal(ctx.body, &ctx.jsonBody)
				if err != nil {
					res := responses.BadRequest("invalid JSON sent as JSON")
					w.WriteHeader(res.Code)
					json.NewEncoder(w).Encode(res)
					return
				}
			}
		}

		revfail := false
		// rev checking
		if qrev != "" {
			if hrev != "" && qrev != hrev {
				revfail = true
			} else {
				ctx.providedRev = qrev
			}
		}
		drev = ""
		if drevi, ok := ctx.jsonBody["_rev"]; ok {
			drev = drevi.(string)
		}
		if drev != "" {
			if qrev != "" && qrev != drev {
				revfail = true
			} else if hrev != "" && hrev != drev {
				revfail = true
			} else if qrev != "" && hrev != "" && qrev != hrev {
				revfail = true
			} else {
				ctx.providedRev = drev
			}
		}

		if revfail {
			res := responses.BadRequest("different rev values were sent")
			w.WriteHeader(res.Code)
			json.NewEncoder(w).Encode(res)
			return
		}

		ctx.exists = ctx.currentRev != "" && !ctx.deleted
		context.Set(r, k, ctx)

		next.ServeHTTP(w, r)
	})
}