// 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) }
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}) }
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) }
/* 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}) }
/* 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}) }
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}) }
/* 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) }
// 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) }
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) }) }