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