1package couchdb 2 3import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "mime" 11 "mime/multipart" 12 "net/http" 13 "net/url" 14 "strconv" 15 "strings" 16 17 "github.com/go-kivik/couchdb/chttp" 18 "github.com/go-kivik/kivik" 19 "github.com/go-kivik/kivik/driver" 20 "github.com/go-kivik/kivik/errors" 21) 22 23type db struct { 24 *client 25 dbName string 26} 27 28var _ driver.DB = &db{} 29var _ driver.MetaGetter = &db{} 30var _ driver.AttachmentMetaGetter = &db{} 31 32func (d *db) path(path string, query url.Values) string { 33 url, _ := url.Parse(d.dbName + "/" + strings.TrimPrefix(path, "/")) 34 if query != nil { 35 url.RawQuery = query.Encode() 36 } 37 return url.String() 38} 39 40func optionsToParams(opts ...map[string]interface{}) (url.Values, error) { 41 params := url.Values{} 42 for _, optsSet := range opts { 43 for key, i := range optsSet { 44 var values []string 45 switch v := i.(type) { 46 case string: 47 values = []string{v} 48 case []string: 49 values = v 50 case bool: 51 values = []string{fmt.Sprintf("%t", v)} 52 case int, uint, uint8, uint16, uint32, uint64, int8, int16, int32, int64: 53 values = []string{fmt.Sprintf("%d", v)} 54 default: 55 return nil, errors.Statusf(kivik.StatusBadRequest, "kivik: invalid type %T for options", i) 56 } 57 for _, value := range values { 58 params.Add(key, value) 59 } 60 } 61 } 62 return params, nil 63} 64 65// rowsQuery performs a query that returns a rows iterator. 66func (d *db) rowsQuery(ctx context.Context, path string, opts map[string]interface{}) (driver.Rows, error) { 67 options, err := optionsToParams(opts) 68 if err != nil { 69 return nil, err 70 } 71 resp, err := d.Client.DoReq(ctx, kivik.MethodGet, d.path(path, options), nil) 72 if err != nil { 73 return nil, err 74 } 75 if err = chttp.ResponseError(resp); err != nil { 76 return nil, err 77 } 78 return newRows(resp.Body), nil 79} 80 81// AllDocs returns all of the documents in the database. 82func (d *db) AllDocs(ctx context.Context, opts map[string]interface{}) (driver.Rows, error) { 83 return d.rowsQuery(ctx, "_all_docs", opts) 84} 85 86// Query queries a view. 87func (d *db) Query(ctx context.Context, ddoc, view string, opts map[string]interface{}) (driver.Rows, error) { 88 return d.rowsQuery(ctx, fmt.Sprintf("_design/%s/_view/%s", chttp.EncodeDocID(ddoc), chttp.EncodeDocID(view)), opts) 89} 90 91// Get fetches the requested document. 92func (d *db) Get(ctx context.Context, docID string, options map[string]interface{}) (*driver.Document, error) { 93 resp, rev, err := d.get(ctx, http.MethodGet, docID, options) 94 if err != nil { 95 return nil, err 96 } 97 ct, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) 98 if err != nil { 99 return nil, errors.WrapStatus(kivik.StatusBadResponse, err) 100 } 101 switch ct { 102 case "application/json": 103 return &driver.Document{ 104 Rev: rev, 105 ContentLength: resp.ContentLength, 106 Body: resp.Body, 107 }, nil 108 case "multipart/related": 109 boundary := strings.Trim(params["boundary"], "\"") 110 if boundary == "" { 111 return nil, errors.Statusf(kivik.StatusBadResponse, "kivik: boundary missing for multipart/related response") 112 } 113 mpReader := multipart.NewReader(resp.Body, boundary) 114 body, err := mpReader.NextPart() 115 if err != nil { 116 return nil, errors.WrapStatus(kivik.StatusBadResponse, err) 117 } 118 length := int64(-1) 119 if cl, e := strconv.ParseInt(body.Header.Get("Content-Length"), 10, 64); e == nil { 120 length = cl 121 } 122 123 // TODO: Use a TeeReader here, to avoid slurping the entire body into memory at once 124 content, err := ioutil.ReadAll(body) 125 if err != nil { 126 return nil, errors.WrapStatus(kivik.StatusBadResponse, err) 127 } 128 var metaDoc struct { 129 Attachments map[string]attMeta `json:"_attachments"` 130 } 131 if err := json.Unmarshal(content, &metaDoc); err != nil { 132 return nil, errors.WrapStatus(kivik.StatusBadResponse, err) 133 } 134 135 return &driver.Document{ 136 ContentLength: length, 137 Rev: rev, 138 Body: ioutil.NopCloser(bytes.NewBuffer(content)), 139 Attachments: &multipartAttachments{ 140 content: resp.Body, 141 mpReader: mpReader, 142 meta: metaDoc.Attachments, 143 }, 144 }, nil 145 default: 146 return nil, errors.Statusf(kivik.StatusBadResponse, "kivik: invalid content type in response: %s", ct) 147 } 148} 149 150type attMeta struct { 151 ContentType string `json:"content_type"` 152 Size *int64 `json:"length"` 153 Follows bool `json:"follows"` 154} 155 156type multipartAttachments struct { 157 content io.ReadCloser 158 mpReader *multipart.Reader 159 meta map[string]attMeta 160} 161 162var _ driver.Attachments = &multipartAttachments{} 163 164func (a *multipartAttachments) Next(att *driver.Attachment) error { 165 part, err := a.mpReader.NextPart() 166 switch err { 167 case io.EOF: 168 return err 169 case nil: 170 // fall through 171 default: 172 return errors.WrapStatus(kivik.StatusBadResponse, err) 173 } 174 175 disp, dispositionParams, err := mime.ParseMediaType(part.Header.Get("Content-Disposition")) 176 if err != nil { 177 return errors.WrapStatus(kivik.StatusBadResponse, errors.Wrap(err, "Content-Disposition")) 178 } 179 if disp != "attachment" { 180 return errors.Statusf(kivik.StatusBadResponse, "Unexpected Content-Disposition: %s", disp) 181 } 182 filename := dispositionParams["filename"] 183 184 meta := a.meta[filename] 185 if !meta.Follows { 186 return errors.Statusf(kivik.StatusBadResponse, "File '%s' not in manifest", filename) 187 } 188 189 size := int64(-1) 190 if meta.Size != nil { 191 size = *meta.Size 192 } else if cl, e := strconv.ParseInt(part.Header.Get("Content-Length"), 10, 64); e == nil { 193 size = cl 194 } 195 196 var cType string 197 if ctHeader, ok := part.Header["Content-Type"]; ok { 198 cType, _, err = mime.ParseMediaType(ctHeader[0]) 199 if err != nil { 200 return errors.WrapStatus(kivik.StatusBadResponse, err) 201 } 202 } else { 203 cType = meta.ContentType 204 } 205 206 *att = driver.Attachment{ 207 Filename: filename, 208 Size: size, 209 ContentType: cType, 210 Content: part, 211 } 212 return nil 213} 214 215func (a *multipartAttachments) Close() error { 216 return a.content.Close() 217} 218 219// Rev returns the most current rev of the requested document. 220func (d *db) GetMeta(ctx context.Context, docID string, options map[string]interface{}) (size int64, rev string, err error) { 221 resp, rev, err := d.get(ctx, http.MethodHead, docID, options) 222 if err != nil { 223 return 0, "", err 224 } 225 return resp.ContentLength, rev, err 226} 227 228func (d *db) get(ctx context.Context, method string, docID string, options map[string]interface{}) (*http.Response, string, error) { 229 if docID == "" { 230 return nil, "", missingArg("docID") 231 } 232 233 inm, err := ifNoneMatch(options) 234 if err != nil { 235 return nil, "", err 236 } 237 238 params, err := optionsToParams(options) 239 if err != nil { 240 return nil, "", err 241 } 242 opts := &chttp.Options{ 243 Accept: "application/json", 244 IfNoneMatch: inm, 245 } 246 resp, err := d.Client.DoReq(ctx, method, d.path(chttp.EncodeDocID(docID), params), opts) 247 if err != nil { 248 return nil, "", err 249 } 250 if respErr := chttp.ResponseError(resp); respErr != nil { 251 return nil, "", respErr 252 } 253 rev, err := chttp.GetRev(resp) 254 return resp, rev, err 255} 256 257func (d *db) CreateDoc(ctx context.Context, doc interface{}, options map[string]interface{}) (docID, rev string, err error) { 258 result := struct { 259 ID string `json:"id"` 260 Rev string `json:"rev"` 261 }{} 262 263 fullCommit, err := fullCommit(false, options) 264 if err != nil { 265 return "", "", err 266 } 267 268 path := d.dbName 269 if len(options) > 0 { 270 params, e := optionsToParams(options) 271 if e != nil { 272 return "", "", e 273 } 274 path += "?" + params.Encode() 275 } 276 277 opts := &chttp.Options{ 278 Body: chttp.EncodeBody(doc), 279 FullCommit: fullCommit, 280 } 281 _, err = d.Client.DoJSON(ctx, kivik.MethodPost, path, opts, &result) 282 return result.ID, result.Rev, err 283} 284 285func (d *db) Put(ctx context.Context, docID string, doc interface{}, options map[string]interface{}) (rev string, err error) { 286 if docID == "" { 287 return "", missingArg("docID") 288 } 289 fullCommit, err := fullCommit(false, options) 290 if err != nil { 291 return "", err 292 } 293 opts := &chttp.Options{ 294 Body: chttp.EncodeBody(doc), 295 FullCommit: fullCommit, 296 } 297 var result struct { 298 ID string `json:"id"` 299 Rev string `json:"rev"` 300 } 301 _, err = d.Client.DoJSON(ctx, kivik.MethodPut, d.path(chttp.EncodeDocID(docID), nil), opts, &result) 302 if err != nil { 303 return "", err 304 } 305 if result.ID != docID { 306 // This should never happen; this is mostly for debugging and internal use 307 return result.Rev, errors.Statusf(kivik.StatusBadResponse, "modified document ID (%s) does not match that requested (%s)", result.ID, docID) 308 } 309 return result.Rev, nil 310} 311 312func (d *db) Delete(ctx context.Context, docID, rev string, options map[string]interface{}) (string, error) { 313 if docID == "" { 314 return "", missingArg("docID") 315 } 316 if rev == "" { 317 return "", missingArg("rev") 318 } 319 320 fullCommit, err := fullCommit(false, options) 321 if err != nil { 322 return "", err 323 } 324 325 query, err := optionsToParams(options) 326 if err != nil { 327 return "", err 328 } 329 query.Add("rev", rev) 330 opts := &chttp.Options{ 331 FullCommit: fullCommit, 332 } 333 resp, err := d.Client.DoReq(ctx, kivik.MethodDelete, d.path(chttp.EncodeDocID(docID), query), opts) 334 if err != nil { 335 return "", err 336 } 337 defer func() { _ = resp.Body.Close() }() 338 return chttp.GetRev(resp) 339} 340 341func (d *db) Flush(ctx context.Context) error { 342 _, err := d.Client.DoError(ctx, kivik.MethodPost, d.path("/_ensure_full_commit", nil), nil) 343 return err 344} 345 346func (d *db) Stats(ctx context.Context) (*driver.DBStats, error) { 347 result := struct { 348 driver.DBStats 349 Sizes struct { 350 File int64 `json:"file"` 351 External int64 `json:"external"` 352 Active int64 `json:"active"` 353 } `json:"sizes"` 354 UpdateSeq json.RawMessage `json:"update_seq"` 355 }{} 356 _, err := d.Client.DoJSON(ctx, kivik.MethodGet, d.dbName, nil, &result) 357 stats := result.DBStats 358 if result.Sizes.File > 0 { 359 stats.DiskSize = result.Sizes.File 360 } 361 if result.Sizes.External > 0 { 362 stats.ExternalSize = result.Sizes.External 363 } 364 if result.Sizes.Active > 0 { 365 stats.ActiveSize = result.Sizes.Active 366 } 367 stats.UpdateSeq = string(bytes.Trim(result.UpdateSeq, `"`)) 368 return &stats, err 369} 370 371func (d *db) Compact(ctx context.Context) error { 372 res, err := d.Client.DoReq(ctx, kivik.MethodPost, d.path("/_compact", nil), nil) 373 if err != nil { 374 return err 375 } 376 return chttp.ResponseError(res) 377} 378 379func (d *db) CompactView(ctx context.Context, ddocID string) error { 380 if ddocID == "" { 381 return missingArg("ddocID") 382 } 383 res, err := d.Client.DoReq(ctx, kivik.MethodPost, d.path("/_compact/"+ddocID, nil), nil) 384 if err != nil { 385 return err 386 } 387 return chttp.ResponseError(res) 388} 389 390func (d *db) ViewCleanup(ctx context.Context) error { 391 res, err := d.Client.DoReq(ctx, kivik.MethodPost, d.path("/_view_cleanup", nil), nil) 392 if err != nil { 393 return err 394 } 395 return chttp.ResponseError(res) 396} 397 398func (d *db) Security(ctx context.Context) (*driver.Security, error) { 399 var sec *driver.Security 400 _, err := d.Client.DoJSON(ctx, kivik.MethodGet, d.path("/_security", nil), nil, &sec) 401 return sec, err 402} 403 404func (d *db) SetSecurity(ctx context.Context, security *driver.Security) error { 405 opts := &chttp.Options{ 406 Body: chttp.EncodeBody(security), 407 } 408 res, err := d.Client.DoReq(ctx, kivik.MethodPut, d.path("/_security", nil), opts) 409 if err != nil { 410 return err 411 } 412 defer func() { _ = res.Body.Close() }() 413 return chttp.ResponseError(res) 414} 415 416func (d *db) Copy(ctx context.Context, targetID, sourceID string, options map[string]interface{}) (targetRev string, err error) { 417 if sourceID == "" { 418 return "", errors.Status(kivik.StatusBadRequest, "kivik: sourceID required") 419 } 420 if targetID == "" { 421 return "", errors.Status(kivik.StatusBadRequest, "kivik: targetID required") 422 } 423 fullCommit, err := fullCommit(false, options) 424 if err != nil { 425 return "", err 426 } 427 params, err := optionsToParams(options) 428 if err != nil { 429 return "", err 430 } 431 opts := &chttp.Options{ 432 FullCommit: fullCommit, 433 Destination: targetID, 434 } 435 resp, err := d.Client.DoReq(ctx, kivik.MethodCopy, d.path(chttp.EncodeDocID(sourceID), params), opts) 436 if err != nil { 437 return "", err 438 } 439 defer resp.Body.Close() // nolint: errcheck 440 return chttp.GetRev(resp) 441} 442