github.com/go-kivik/kivik/v4@v4.3.2/couchdb/db.go (about)

     1  // Licensed under the Apache License, Version 2.0 (the "License"); you may not
     2  // use this file except in compliance with the License. You may obtain a copy of
     3  // the License at
     4  //
     5  //  http://www.apache.org/licenses/LICENSE-2.0
     6  //
     7  // Unless required by applicable law or agreed to in writing, software
     8  // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     9  // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    10  // License for the specific language governing permissions and limitations under
    11  // the License.
    12  
    13  package couchdb
    14  
    15  import (
    16  	"bytes"
    17  	"context"
    18  	"encoding/json"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"mime"
    23  	"mime/multipart"
    24  	"net/http"
    25  	"net/textproto"
    26  	"net/url"
    27  	"os"
    28  	"path/filepath"
    29  	"reflect"
    30  	"sort"
    31  	"strconv"
    32  	"strings"
    33  	"sync"
    34  	"sync/atomic"
    35  
    36  	kivik "github.com/go-kivik/kivik/v4"
    37  	"github.com/go-kivik/kivik/v4/couchdb/chttp"
    38  	"github.com/go-kivik/kivik/v4/driver"
    39  	internal "github.com/go-kivik/kivik/v4/int/errors"
    40  )
    41  
    42  type db struct {
    43  	*client
    44  	dbName string
    45  }
    46  
    47  var (
    48  	_ driver.DB                   = &db{}
    49  	_ driver.Finder               = &db{}
    50  	_ driver.RevGetter            = &db{}
    51  	_ driver.AttachmentMetaGetter = &db{}
    52  	_ driver.PartitionedDB        = &db{}
    53  )
    54  
    55  func (d *db) path(path string) string {
    56  	url, err := url.Parse(d.dbName + "/" + strings.TrimPrefix(path, "/"))
    57  	if err != nil {
    58  		panic("THIS IS A BUG: d.path failed: " + err.Error())
    59  	}
    60  	return url.String()
    61  }
    62  
    63  func optionsToParams(opts ...map[string]interface{}) (url.Values, error) {
    64  	params := url.Values{}
    65  	for _, optsSet := range opts {
    66  		if err := encodeKeys(optsSet); err != nil {
    67  			return nil, err
    68  		}
    69  		for key, i := range optsSet {
    70  			var values []string
    71  			switch v := i.(type) {
    72  			case string:
    73  				values = []string{v}
    74  			case []string:
    75  				values = v
    76  			case bool:
    77  				values = []string{fmt.Sprintf("%t", v)}
    78  			case int, uint, uint8, uint16, uint32, uint64, int8, int16, int32, int64:
    79  				values = []string{fmt.Sprintf("%d", v)}
    80  			default:
    81  				return nil, &internal.Error{Status: http.StatusBadRequest, Err: fmt.Errorf("kivik: invalid type %T for options", i)}
    82  			}
    83  			for _, value := range values {
    84  				params.Add(key, value)
    85  			}
    86  		}
    87  	}
    88  	return params, nil
    89  }
    90  
    91  // rowsQuery performs a query that returns a rows iterator.
    92  func (d *db) rowsQuery(ctx context.Context, path string, options driver.Options) (driver.Rows, error) {
    93  	opts := map[string]interface{}{}
    94  	options.Apply(opts)
    95  	payload := make(map[string]interface{})
    96  	if keys := opts["keys"]; keys != nil {
    97  		delete(opts, "keys")
    98  		payload["keys"] = keys
    99  	}
   100  	rowsInit := newRows
   101  	if queries := opts["queries"]; queries != nil {
   102  		rowsInit = newMultiQueriesRows
   103  		delete(opts, "queries")
   104  		payload["queries"] = queries
   105  		// Funny that this works even in CouchDB 1.x. It seems 1.x just ignores
   106  		// extra path elements beyond the view name. So yay for accidental
   107  		// backward compatibility!
   108  		path = filepath.Join(path, "queries")
   109  	}
   110  	query, err := optionsToParams(opts)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	chttpOpts := &chttp.Options{Query: query}
   115  	method := http.MethodGet
   116  	if len(payload) > 0 {
   117  		method = http.MethodPost
   118  		chttpOpts.GetBody = chttp.BodyEncoder(payload)
   119  		chttpOpts.Header = http.Header{
   120  			chttp.HeaderIdempotencyKey: []string{},
   121  		}
   122  	}
   123  	resp, err := d.Client.DoReq(ctx, method, d.path(path), chttpOpts)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	if err = chttp.ResponseError(resp); err != nil {
   128  		return nil, err
   129  	}
   130  	return rowsInit(ctx, resp.Body), nil
   131  }
   132  
   133  // AllDocs returns all of the documents in the database.
   134  func (d *db) AllDocs(ctx context.Context, options driver.Options) (driver.Rows, error) {
   135  	reqPath := partPath("_all_docs")
   136  	options.Apply(reqPath)
   137  	return d.rowsQuery(ctx, reqPath.String(), options)
   138  }
   139  
   140  // DesignDocs returns all of the documents in the database.
   141  func (d *db) DesignDocs(ctx context.Context, options driver.Options) (driver.Rows, error) {
   142  	return d.rowsQuery(ctx, "_design_docs", options)
   143  }
   144  
   145  // LocalDocs returns all of the documents in the database.
   146  func (d *db) LocalDocs(ctx context.Context, options driver.Options) (driver.Rows, error) {
   147  	return d.rowsQuery(ctx, "_local_docs", options)
   148  }
   149  
   150  // Query queries a view.
   151  func (d *db) Query(ctx context.Context, ddoc, view string, options driver.Options) (driver.Rows, error) {
   152  	reqPath := partPath(fmt.Sprintf("_design/%s/_view/%s", chttp.EncodeDocID(ddoc), chttp.EncodeDocID(view)))
   153  	options.Apply(reqPath)
   154  	return d.rowsQuery(ctx, reqPath.String(), options)
   155  }
   156  
   157  // document represents a single document returned by Get
   158  type document struct {
   159  	id          string
   160  	rev         string
   161  	body        io.ReadCloser
   162  	attachments driver.Attachments
   163  
   164  	// read will be non-zero once the document has been read.
   165  	read int32
   166  }
   167  
   168  func (d *document) Next(row *driver.Row) error {
   169  	if atomic.SwapInt32(&d.read, 1) > 0 {
   170  		return io.EOF
   171  	}
   172  	row.ID = d.id
   173  	row.Rev = d.rev
   174  	row.Doc = d.body
   175  	row.Attachments = d.attachments
   176  	return nil
   177  }
   178  
   179  func (d *document) Close() error {
   180  	atomic.StoreInt32(&d.read, 1)
   181  	return d.body.Close()
   182  }
   183  
   184  func (*document) UpdateSeq() string { return "" }
   185  func (*document) Offset() int64     { return 0 }
   186  func (*document) TotalRows() int64  { return 0 }
   187  
   188  // Get fetches the requested document.
   189  func (d *db) Get(ctx context.Context, docID string, options driver.Options) (*driver.Document, error) {
   190  	resp, err := d.get(ctx, http.MethodGet, docID, options)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	ct, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
   195  	if err != nil {
   196  		return nil, &internal.Error{Status: http.StatusBadGateway, Err: err}
   197  	}
   198  
   199  	switch ct {
   200  	case typeJSON, typeMPRelated:
   201  		etag, _ := chttp.ETag(resp)
   202  		doc, err := processDoc(docID, ct, params["boundary"], etag, resp.Body)
   203  		if err != nil {
   204  			return nil, err
   205  		}
   206  		return &driver.Document{
   207  			Rev:         doc.rev,
   208  			Body:        doc.body,
   209  			Attachments: doc.attachments,
   210  		}, nil
   211  	default:
   212  		return nil, &internal.Error{Status: http.StatusBadGateway, Err: fmt.Errorf("kivik: invalid content type in response: %s", ct)}
   213  	}
   214  }
   215  
   216  func openRevs(revs []string) kivik.Option {
   217  	encoded, _ := json.Marshal(revs)
   218  	return kivik.Param("open_revs", string(encoded))
   219  }
   220  
   221  // TODO: Flesh this out.
   222  func (d *db) OpenRevs(ctx context.Context, docID string, revs []string, options driver.Options) (driver.Rows, error) {
   223  	opts := multiOptions{
   224  		kivik.Option(options),
   225  		openRevs(revs),
   226  	}
   227  	resp, err := d.get(ctx, http.MethodGet, docID, opts)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  	ct, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
   232  	if err != nil {
   233  		return nil, &internal.Error{Status: http.StatusBadGateway, Err: err}
   234  	}
   235  
   236  	switch ct {
   237  	case typeJSON, typeMPRelated:
   238  		etag, _ := chttp.ETag(resp)
   239  		return processDoc(docID, ct, params["boundary"], etag, resp.Body)
   240  	case typeMPMixed:
   241  		boundary := strings.Trim(params["boundary"], "\"")
   242  		if boundary == "" {
   243  			return nil, &internal.Error{Status: http.StatusBadGateway, Err: errors.New("kivik: boundary missing for multipart/related response")}
   244  		}
   245  		mpReader := multipart.NewReader(resp.Body, boundary)
   246  		return &multiDocs{
   247  			id:       docID,
   248  			respBody: resp.Body,
   249  			reader:   mpReader,
   250  		}, nil
   251  	default:
   252  		return nil, &internal.Error{Status: http.StatusBadGateway, Err: fmt.Errorf("kivik: invalid content type in response: %s", ct)}
   253  	}
   254  }
   255  
   256  func processDoc(docID, ct, boundary, rev string, body io.ReadCloser) (*document, error) {
   257  	switch ct {
   258  	case typeJSON:
   259  		if rev == "" {
   260  			var err error
   261  			body, rev, err = chttp.ExtractRev(body)
   262  			if err != nil {
   263  				return nil, err
   264  			}
   265  		}
   266  
   267  		return &document{
   268  			id:   docID,
   269  			rev:  rev,
   270  			body: body,
   271  		}, nil
   272  	case typeMPRelated:
   273  		boundary := strings.Trim(boundary, "\"")
   274  		if boundary == "" {
   275  			return nil, &internal.Error{Status: http.StatusBadGateway, Err: errors.New("kivik: boundary missing for multipart/related response")}
   276  		}
   277  		mpReader := multipart.NewReader(body, boundary)
   278  		body, err := mpReader.NextPart()
   279  		if err != nil {
   280  			return nil, &internal.Error{Status: http.StatusBadGateway, Err: err}
   281  		}
   282  
   283  		// TODO: Use a TeeReader here, to avoid slurping the entire body into memory at once
   284  		content, err := io.ReadAll(body)
   285  		if err != nil {
   286  			return nil, &internal.Error{Status: http.StatusBadGateway, Err: err}
   287  		}
   288  		_, rev, err := chttp.ExtractRev(io.NopCloser(bytes.NewReader(content)))
   289  		if err != nil {
   290  			return nil, err
   291  		}
   292  
   293  		var metaDoc struct {
   294  			Attachments map[string]attMeta `json:"_attachments"`
   295  		}
   296  		if err := json.Unmarshal(content, &metaDoc); err != nil {
   297  			return nil, &internal.Error{Status: http.StatusBadGateway, Err: err}
   298  		}
   299  
   300  		return &document{
   301  			id:   docID,
   302  			rev:  rev,
   303  			body: io.NopCloser(bytes.NewBuffer(content)),
   304  			attachments: &multipartAttachments{
   305  				content:  body,
   306  				mpReader: mpReader,
   307  				meta:     metaDoc.Attachments,
   308  			},
   309  		}, nil
   310  	default:
   311  		return nil, &internal.Error{Status: http.StatusBadGateway, Err: fmt.Errorf("kivik: invalid content type in response: %s", ct)}
   312  	}
   313  }
   314  
   315  type multiDocs struct {
   316  	id           string
   317  	respBody     io.Closer
   318  	reader       *multipart.Reader
   319  	readerCloser io.Closer
   320  }
   321  
   322  var _ driver.Rows = (*multiDocs)(nil)
   323  
   324  func (d *multiDocs) Next(row *driver.Row) error {
   325  	if d.readerCloser != nil {
   326  		if err := d.readerCloser.Close(); err != nil {
   327  			return err
   328  		}
   329  		d.readerCloser = nil
   330  	}
   331  	part, err := d.reader.NextPart()
   332  	if err != nil {
   333  		return err
   334  	}
   335  	ct, params, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
   336  	if err != nil {
   337  		return err
   338  	}
   339  
   340  	if _, ok := params["error"]; ok {
   341  		var body struct {
   342  			Rev string `json:"missing"`
   343  		}
   344  		err := json.NewDecoder(part).Decode(&body)
   345  		row.ID = d.id
   346  		row.Error = &internal.Error{Status: http.StatusNotFound, Err: errors.New("missing")}
   347  		row.Rev = body.Rev
   348  		return err
   349  	}
   350  
   351  	doc, err := processDoc(d.id, ct, params["boundary"], "", part)
   352  	if err != nil {
   353  		return err
   354  	}
   355  
   356  	row.ID = doc.id
   357  	row.Doc = doc.body
   358  	row.Rev = doc.rev
   359  	row.Attachments = doc.attachments
   360  
   361  	return nil
   362  }
   363  
   364  func (d *multiDocs) Close() error {
   365  	if d.readerCloser != nil {
   366  		if err := d.readerCloser.Close(); err != nil {
   367  			return err
   368  		}
   369  		d.readerCloser = nil
   370  	}
   371  	return d.respBody.Close()
   372  }
   373  
   374  func (*multiDocs) UpdateSeq() string { return "" }
   375  func (*multiDocs) Offset() int64     { return 0 }
   376  func (*multiDocs) TotalRows() int64  { return 0 }
   377  
   378  type attMeta struct {
   379  	ContentType string `json:"content_type"`
   380  	Size        *int64 `json:"length"`
   381  	Follows     bool   `json:"follows"`
   382  }
   383  
   384  type multipartAttachments struct {
   385  	content  io.ReadCloser
   386  	mpReader *multipart.Reader
   387  	meta     map[string]attMeta
   388  }
   389  
   390  var _ driver.Attachments = &multipartAttachments{}
   391  
   392  func (a *multipartAttachments) Next(att *driver.Attachment) error {
   393  	part, err := a.mpReader.NextPart()
   394  	switch err {
   395  	case io.EOF:
   396  		return err
   397  	case nil:
   398  		// fall through
   399  	default:
   400  		return &internal.Error{Status: http.StatusBadGateway, Err: err}
   401  	}
   402  
   403  	disp, dispositionParams, err := mime.ParseMediaType(part.Header.Get("Content-Disposition"))
   404  	if err != nil {
   405  		return &internal.Error{Status: http.StatusBadGateway, Err: fmt.Errorf("Content-Disposition: %s", err)}
   406  	}
   407  	if disp != "attachment" {
   408  		return &internal.Error{Status: http.StatusBadGateway, Err: fmt.Errorf("Unexpected Content-Disposition: %s", disp)}
   409  	}
   410  	filename := dispositionParams["filename"]
   411  
   412  	meta := a.meta[filename]
   413  	if !meta.Follows {
   414  		return &internal.Error{Status: http.StatusBadGateway, Err: fmt.Errorf("File '%s' not in manifest", filename)}
   415  	}
   416  
   417  	size := int64(-1)
   418  	if meta.Size != nil {
   419  		size = *meta.Size
   420  	} else if cl, e := strconv.ParseInt(part.Header.Get("Content-Length"), 10, 64); e == nil { // nolint:gomnd
   421  		size = cl
   422  	}
   423  
   424  	var cType string
   425  	if ctHeader, ok := part.Header["Content-Type"]; ok {
   426  		cType, _, err = mime.ParseMediaType(ctHeader[0])
   427  		if err != nil {
   428  			return &internal.Error{Status: http.StatusBadGateway, Err: err}
   429  		}
   430  	} else {
   431  		cType = meta.ContentType
   432  	}
   433  
   434  	*att = driver.Attachment{
   435  		Filename:        filename,
   436  		Size:            size,
   437  		ContentType:     cType,
   438  		Content:         part,
   439  		ContentEncoding: part.Header.Get("Content-Encoding"),
   440  	}
   441  	return nil
   442  }
   443  
   444  func (a *multipartAttachments) Close() error {
   445  	return a.content.Close()
   446  }
   447  
   448  // Rev returns the most current rev of the requested document.
   449  func (d *db) GetRev(ctx context.Context, docID string, options driver.Options) (string, error) {
   450  	resp, err := d.get(ctx, http.MethodHead, docID, options)
   451  	if err != nil {
   452  		return "", err
   453  	}
   454  	_ = resp.Body.Close()
   455  	rev, err := chttp.GetRev(resp)
   456  	if err != nil {
   457  		return "", err
   458  	}
   459  	return rev, err
   460  }
   461  
   462  type getOptions struct {
   463  	noMultipartGet bool
   464  }
   465  
   466  func (d *db) get(ctx context.Context, method, docID string, options driver.Options) (*http.Response, error) {
   467  	if docID == "" {
   468  		return nil, missingArg("docID")
   469  	}
   470  
   471  	var getOpts getOptions
   472  	options.Apply(&getOpts)
   473  
   474  	opts := map[string]interface{}{}
   475  	options.Apply(opts)
   476  
   477  	chttpOpts := chttp.NewOptions(options)
   478  
   479  	chttpOpts.Accept = strings.Join([]string{typeMPMixed, typeMPRelated, typeJSON}, ", ")
   480  	var err error
   481  	chttpOpts.Query, err = optionsToParams(opts)
   482  	if err != nil {
   483  		return nil, err
   484  	}
   485  	if getOpts.noMultipartGet {
   486  		chttpOpts.Accept = typeJSON
   487  	}
   488  	resp, err := d.Client.DoReq(ctx, method, d.path(chttp.EncodeDocID(docID)), chttpOpts)
   489  	if err != nil {
   490  		return nil, err
   491  	}
   492  	err = chttp.ResponseError(resp)
   493  	return resp, err
   494  }
   495  
   496  func (d *db) CreateDoc(ctx context.Context, doc interface{}, options driver.Options) (docID, rev string, err error) {
   497  	result := struct {
   498  		ID  string `json:"id"`
   499  		Rev string `json:"rev"`
   500  	}{}
   501  
   502  	chttpOpts := chttp.NewOptions(options)
   503  
   504  	opts := map[string]interface{}{}
   505  	options.Apply(opts)
   506  
   507  	path := d.dbName
   508  	if len(opts) > 0 {
   509  		params, e := optionsToParams(opts)
   510  		if e != nil {
   511  			return "", "", e
   512  		}
   513  		path += "?" + params.Encode()
   514  	}
   515  
   516  	chttpOpts.Body = chttp.EncodeBody(doc)
   517  
   518  	err = d.Client.DoJSON(ctx, http.MethodPost, path, chttpOpts, &result)
   519  	return result.ID, result.Rev, err
   520  }
   521  
   522  type putOptions struct {
   523  	NoMultipartPut bool
   524  }
   525  
   526  func putOpts(doc interface{}, options driver.Options) (*chttp.Options, error) {
   527  	chttpOpts := chttp.NewOptions(options)
   528  	opts := map[string]interface{}{}
   529  	options.Apply(opts)
   530  	var err error
   531  	chttpOpts.Query, err = optionsToParams(opts)
   532  	if err != nil {
   533  		return nil, err
   534  	}
   535  	var putOpts putOptions
   536  	options.Apply(&putOpts)
   537  	if putOpts.NoMultipartPut {
   538  		if atts, ok := extractAttachments(doc); ok {
   539  			boundary, size, multipartBody, err := newMultipartAttachments(chttp.EncodeBody(doc), atts)
   540  			if err != nil {
   541  				return nil, err
   542  			}
   543  			chttpOpts.Body = multipartBody
   544  			chttpOpts.ContentLength = size
   545  			chttpOpts.ContentType = fmt.Sprintf(typeMPRelated+"; boundary=%q", boundary)
   546  			return chttpOpts, nil
   547  		}
   548  	}
   549  	chttpOpts.Body = chttp.EncodeBody(doc)
   550  	return chttpOpts, nil
   551  }
   552  
   553  func (d *db) Put(ctx context.Context, docID string, doc interface{}, options driver.Options) (rev string, err error) {
   554  	if docID == "" {
   555  		return "", missingArg("docID")
   556  	}
   557  	opts2, err := putOpts(doc, options)
   558  	if err != nil {
   559  		return "", err
   560  	}
   561  	var result struct {
   562  		ID  string `json:"id"`
   563  		Rev string `json:"rev"`
   564  	}
   565  	err = d.Client.DoJSON(ctx, http.MethodPut, d.path(chttp.EncodeDocID(docID)), opts2, &result)
   566  	if err != nil {
   567  		return "", err
   568  	}
   569  	return result.Rev, nil
   570  }
   571  
   572  const attachmentsKey = "_attachments"
   573  
   574  func extractAttachments(doc interface{}) (*kivik.Attachments, bool) {
   575  	if doc == nil {
   576  		return nil, false
   577  	}
   578  	v := reflect.ValueOf(doc)
   579  	if v.Type().Kind() == reflect.Ptr {
   580  		return extractAttachments(v.Elem().Interface())
   581  	}
   582  	if stdMap, ok := doc.(map[string]interface{}); ok {
   583  		return interfaceToAttachments(stdMap[attachmentsKey])
   584  	}
   585  	if v.Kind() != reflect.Struct {
   586  		return nil, false
   587  	}
   588  	for i := 0; i < v.NumField(); i++ {
   589  		if v.Type().Field(i).Tag.Get("json") == attachmentsKey {
   590  			return interfaceToAttachments(v.Field(i).Interface())
   591  		}
   592  	}
   593  	return nil, false
   594  }
   595  
   596  func interfaceToAttachments(i interface{}) (*kivik.Attachments, bool) {
   597  	switch t := i.(type) {
   598  	case kivik.Attachments:
   599  		atts := make(kivik.Attachments, len(t))
   600  		for k, v := range t {
   601  			atts[k] = v
   602  			delete(t, k)
   603  		}
   604  		return &atts, true
   605  	case *kivik.Attachments:
   606  		atts := new(kivik.Attachments)
   607  		*atts = *t
   608  		*t = nil
   609  		return atts, true
   610  	}
   611  	return nil, false
   612  }
   613  
   614  // newMultipartAttachments reads a json stream on in, and produces a
   615  // multipart/related output suitable for a PUT request.
   616  func newMultipartAttachments(in io.ReadCloser, att *kivik.Attachments) (boundary string, size int64, content io.ReadCloser, err error) {
   617  	tmp, err := os.CreateTemp("", "kivik-multipart-*")
   618  	if err != nil {
   619  		return "", 0, nil, err
   620  	}
   621  	body := multipart.NewWriter(tmp)
   622  	w := sync.WaitGroup{}
   623  	w.Add(1)
   624  	go func() {
   625  		err = createMultipart(body, in, att)
   626  		e := in.Close()
   627  		if err == nil {
   628  			err = e
   629  		}
   630  		w.Done()
   631  	}()
   632  	w.Wait()
   633  	if e := tmp.Sync(); err == nil {
   634  		err = e
   635  	}
   636  	if info, e := tmp.Stat(); e == nil {
   637  		size = info.Size()
   638  	} else if err == nil {
   639  		err = e
   640  	}
   641  	if _, e := tmp.Seek(0, 0); e != nil && err == nil {
   642  		err = e
   643  	}
   644  	return body.Boundary(),
   645  		size,
   646  		tmp,
   647  		err
   648  }
   649  
   650  func createMultipart(w *multipart.Writer, r io.ReadCloser, atts *kivik.Attachments) error {
   651  	doc, err := w.CreatePart(textproto.MIMEHeader{
   652  		"Content-Type": {typeJSON},
   653  	})
   654  	if err != nil {
   655  		return err
   656  	}
   657  	attJSON := replaceAttachments(r, atts)
   658  	if _, e := io.Copy(doc, attJSON); e != nil {
   659  		return e
   660  	}
   661  
   662  	// Sort the filenames to ensure order consistent with json.Marshal's ordering
   663  	// of the stubs in the body
   664  	filenames := make([]string, 0, len(*atts))
   665  	for filename := range *atts {
   666  		filenames = append(filenames, filename)
   667  	}
   668  	sort.Strings(filenames)
   669  
   670  	for _, filename := range filenames {
   671  		att := (*atts)[filename]
   672  		file, err := w.CreatePart(textproto.MIMEHeader{
   673  			// "Content-Type":        {att.ContentType},
   674  			// "Content-Disposition": {fmt.Sprintf(`attachment; filename=%q`, filename)},
   675  			// "Content-Length":      {strconv.FormatInt(att.Size, 10)},
   676  		})
   677  		if err != nil {
   678  			return err
   679  		}
   680  		if _, err := io.Copy(file, att.Content); err != nil {
   681  			return err
   682  		}
   683  		_ = att.Content.Close()
   684  	}
   685  
   686  	return w.Close()
   687  }
   688  
   689  type lener interface {
   690  	Len() int
   691  }
   692  
   693  type stater interface {
   694  	Stat() (os.FileInfo, error)
   695  }
   696  
   697  // attachmentSize determines the size of the `in` stream, possibly by reading
   698  // the entire stream first. If att.Size is already set, this function does
   699  // nothing. It attempts the following methods:
   700  //
   701  //  1. Calls `Len()`, if implemented by `in` (i.e. `*bytes.Buffer`)
   702  //  2. Calls `Stat()`, if implemented by `in` (i.e. `*os.File`) then returns
   703  //     the file's size
   704  //  3. If `in` is an io.Seeker, copy the entire contents to io.Discard to
   705  //     determine size, then reset the reader to the beginning.
   706  //  4. Read the entire stream to determine the size, and replace att.Content
   707  //     to be replayed.
   708  func attachmentSize(att *kivik.Attachment) error {
   709  	if att.Size > 0 {
   710  		return nil
   711  	}
   712  	size, r, err := readerSize(att.Content)
   713  	if err != nil {
   714  		return err
   715  	}
   716  	rc, ok := r.(io.ReadCloser)
   717  	if !ok {
   718  		rc = io.NopCloser(r)
   719  	}
   720  
   721  	att.Content = rc
   722  	att.Size = size
   723  	return nil
   724  }
   725  
   726  func readerSize(in io.Reader) (int64, io.Reader, error) {
   727  	if ln, ok := in.(lener); ok {
   728  		return int64(ln.Len()), in, nil
   729  	}
   730  	if st, ok := in.(stater); ok {
   731  		info, err := st.Stat()
   732  		if err != nil {
   733  			return 0, nil, err
   734  		}
   735  		return info.Size(), in, nil
   736  	}
   737  	if sk, ok := in.(io.Seeker); ok {
   738  		n, err := io.Copy(io.Discard, in)
   739  		if err != nil {
   740  			return 0, nil, err
   741  		}
   742  		_, err = sk.Seek(0, io.SeekStart)
   743  		return n, in, err
   744  	}
   745  	content, err := io.ReadAll(in)
   746  	if err != nil {
   747  		return 0, nil, err
   748  	}
   749  	buf := bytes.NewBuffer(content)
   750  	return int64(buf.Len()), io.NopCloser(buf), nil
   751  }
   752  
   753  // NewAttachment is a convenience function, which sets the size of the attachment
   754  // based on content. This is intended for creating attachments to be uploaded
   755  // using multipart/related capabilities of [github.com/go-kivik/kivik/v4.DB.Put].
   756  // The attachment size will be set to the first of the following found:
   757  //
   758  //  1. `size`, if present. Only the first value is considered.
   759  //  2. content.Len(), if implemented (i.e. [bytes.Buffer])
   760  //  3. content.Stat().Size(), if implemented (i.e. [os.File])
   761  //  4. Read the entire content into memory, to determine the size. This can
   762  //     use a lot of memory for large attachments. Please use a file, or
   763  //     specify the size directly instead.
   764  func NewAttachment(filename, contentType string, content io.Reader, size ...int64) (*kivik.Attachment, error) {
   765  	var filesize int64
   766  	if len(size) > 0 {
   767  		filesize = size[0]
   768  	} else {
   769  		var err error
   770  		filesize, content, err = readerSize(content)
   771  		if err != nil {
   772  			return nil, err
   773  		}
   774  	}
   775  	rc, ok := content.(io.ReadCloser)
   776  	if !ok {
   777  		rc = io.NopCloser(content)
   778  	}
   779  	return &kivik.Attachment{
   780  		Filename:    filename,
   781  		ContentType: contentType,
   782  		Content:     rc,
   783  		Size:        filesize,
   784  	}, nil
   785  }
   786  
   787  // replaceAttachments reads a JSON stream on in, looking for the _attachments
   788  // key, then replaces its value with the marshaled version of att.
   789  func replaceAttachments(in io.ReadCloser, atts *kivik.Attachments) io.ReadCloser {
   790  	r, w := io.Pipe()
   791  	go func() {
   792  		stubs, err := attachmentStubs(atts)
   793  		if err != nil {
   794  			_ = w.CloseWithError(err)
   795  			_ = in.Close()
   796  			return
   797  		}
   798  		err = copyWithAttachmentStubs(w, in, stubs)
   799  		e := in.Close()
   800  		if err == nil {
   801  			err = e
   802  		}
   803  		_ = w.CloseWithError(err)
   804  	}()
   805  	return r
   806  }
   807  
   808  type stub struct {
   809  	ContentType string `json:"content_type"`
   810  	Size        int64  `json:"length"`
   811  }
   812  
   813  func (s *stub) MarshalJSON() ([]byte, error) {
   814  	type attJSON struct {
   815  		stub
   816  		Follows bool `json:"follows"`
   817  	}
   818  	att := attJSON{
   819  		stub:    *s,
   820  		Follows: true,
   821  	}
   822  	return json.Marshal(att)
   823  }
   824  
   825  func attachmentStubs(atts *kivik.Attachments) (map[string]*stub, error) {
   826  	if atts == nil {
   827  		return nil, nil
   828  	}
   829  	result := make(map[string]*stub, len(*atts))
   830  	for filename, att := range *atts {
   831  		if err := attachmentSize(att); err != nil {
   832  			return nil, err
   833  		}
   834  		result[filename] = &stub{
   835  			ContentType: att.ContentType,
   836  			Size:        att.Size,
   837  		}
   838  	}
   839  	return result, nil
   840  }
   841  
   842  // copyWithAttachmentStubs copies r to w, replacing the _attachment value with the
   843  // marshaled version of atts.
   844  func copyWithAttachmentStubs(w io.Writer, r io.Reader, atts map[string]*stub) error {
   845  	dec := json.NewDecoder(r)
   846  	t, err := dec.Token()
   847  	if err == nil {
   848  		if t != json.Delim('{') {
   849  			return &internal.Error{Status: http.StatusBadRequest, Err: fmt.Errorf("expected '{', found '%v'", t)}
   850  		}
   851  	}
   852  	if err != nil {
   853  		if err != io.EOF {
   854  			return err
   855  		}
   856  	}
   857  	if _, err := fmt.Fprintf(w, "%v", t); err != nil {
   858  		return err
   859  	}
   860  	first := true
   861  	for {
   862  		t, err := dec.Token()
   863  		if err == io.EOF {
   864  			break
   865  		}
   866  		if err != nil {
   867  			return &internal.Error{Status: http.StatusBadRequest, Err: err}
   868  		}
   869  		switch tp := t.(type) {
   870  		case string:
   871  			if !first {
   872  				if _, e := w.Write([]byte(",")); e != nil {
   873  					return e
   874  				}
   875  			}
   876  			first = false
   877  			if _, e := fmt.Fprintf(w, `"%s":`, tp); e != nil {
   878  				return e
   879  			}
   880  			var val json.RawMessage
   881  			if e := dec.Decode(&val); e != nil {
   882  				return e
   883  			}
   884  			if tp == attachmentsKey {
   885  				if e := json.NewEncoder(w).Encode(atts); e != nil {
   886  					return e
   887  				}
   888  				// Once we're here, we can just stream the rest of the input
   889  				// unaltered.
   890  				if _, e := io.Copy(w, dec.Buffered()); e != nil {
   891  					return e
   892  				}
   893  				_, e := io.Copy(w, r)
   894  				return e
   895  			}
   896  			if _, e := w.Write(val); e != nil {
   897  				return e
   898  			}
   899  		case json.Delim:
   900  			if tp != json.Delim('}') {
   901  				return fmt.Errorf("expected '}', found '%v'", t)
   902  			}
   903  			if _, err := fmt.Fprintf(w, "%v", t); err != nil {
   904  				return err
   905  			}
   906  		}
   907  	}
   908  	return nil
   909  }
   910  
   911  func (d *db) Delete(ctx context.Context, docID string, options driver.Options) (string, error) {
   912  	if docID == "" {
   913  		return "", missingArg("docID")
   914  	}
   915  	opts := map[string]interface{}{}
   916  	options.Apply(opts)
   917  	if rev, _ := opts["rev"].(string); rev == "" {
   918  		return "", missingArg("rev")
   919  	}
   920  
   921  	chttpOpts := chttp.NewOptions(options)
   922  
   923  	var err error
   924  	chttpOpts.Query, err = optionsToParams(opts)
   925  	if err != nil {
   926  		return "", err
   927  	}
   928  
   929  	resp, err := d.Client.DoReq(ctx, http.MethodDelete, d.path(chttp.EncodeDocID(docID)), chttpOpts)
   930  	if err != nil {
   931  		return "", err
   932  	}
   933  	defer chttp.CloseBody(resp.Body)
   934  	if err := chttp.ResponseError(resp); err != nil {
   935  		return "", err
   936  	}
   937  	return chttp.GetRev(resp)
   938  }
   939  
   940  func (d *db) Flush(ctx context.Context) error {
   941  	opts := &chttp.Options{
   942  		Header: http.Header{
   943  			chttp.HeaderIdempotencyKey: []string{},
   944  		},
   945  	}
   946  	_, err := d.Client.DoError(ctx, http.MethodPost, d.path("/_ensure_full_commit"), opts)
   947  	return err
   948  }
   949  
   950  func (d *db) Compact(ctx context.Context) error {
   951  	opts := &chttp.Options{
   952  		Header: http.Header{
   953  			chttp.HeaderIdempotencyKey: []string{},
   954  		},
   955  	}
   956  	res, err := d.Client.DoReq(ctx, http.MethodPost, d.path("/_compact"), opts)
   957  	if err != nil {
   958  		return err
   959  	}
   960  	defer chttp.CloseBody(res.Body)
   961  	return chttp.ResponseError(res)
   962  }
   963  
   964  func (d *db) CompactView(ctx context.Context, ddocID string) error {
   965  	if ddocID == "" {
   966  		return missingArg("ddocID")
   967  	}
   968  	opts := &chttp.Options{
   969  		Header: http.Header{
   970  			chttp.HeaderIdempotencyKey: []string{},
   971  		},
   972  	}
   973  	res, err := d.Client.DoReq(ctx, http.MethodPost, d.path("/_compact/"+ddocID), opts)
   974  	if err != nil {
   975  		return err
   976  	}
   977  	defer chttp.CloseBody(res.Body)
   978  	return chttp.ResponseError(res)
   979  }
   980  
   981  func (d *db) ViewCleanup(ctx context.Context) error {
   982  	opts := &chttp.Options{
   983  		Header: http.Header{
   984  			chttp.HeaderIdempotencyKey: []string{},
   985  		},
   986  	}
   987  	res, err := d.Client.DoReq(ctx, http.MethodPost, d.path("/_view_cleanup"), opts)
   988  	if err != nil {
   989  		return err
   990  	}
   991  	defer chttp.CloseBody(res.Body)
   992  	return chttp.ResponseError(res)
   993  }
   994  
   995  func (d *db) Security(ctx context.Context) (*driver.Security, error) {
   996  	var sec *driver.Security
   997  	err := d.Client.DoJSON(ctx, http.MethodGet, d.path("/_security"), nil, &sec)
   998  	return sec, err
   999  }
  1000  
  1001  func (d *db) SetSecurity(ctx context.Context, security *driver.Security) error {
  1002  	opts := &chttp.Options{
  1003  		GetBody: chttp.BodyEncoder(security),
  1004  		Header: http.Header{
  1005  			chttp.HeaderIdempotencyKey: []string{},
  1006  		},
  1007  	}
  1008  	res, err := d.Client.DoReq(ctx, http.MethodPut, d.path("/_security"), opts)
  1009  	if err != nil {
  1010  		return err
  1011  	}
  1012  	defer chttp.CloseBody(res.Body)
  1013  	return chttp.ResponseError(res)
  1014  }
  1015  
  1016  func (d *db) Copy(ctx context.Context, targetID, sourceID string, options driver.Options) (targetRev string, err error) {
  1017  	if sourceID == "" {
  1018  		return "", missingArg("sourceID")
  1019  	}
  1020  	if targetID == "" {
  1021  		return "", missingArg("targetID")
  1022  	}
  1023  	chttpOpts := chttp.NewOptions(options)
  1024  
  1025  	opts := map[string]interface{}{}
  1026  	options.Apply(opts)
  1027  	chttpOpts.Query, err = optionsToParams(opts)
  1028  	if err != nil {
  1029  		return "", err
  1030  	}
  1031  	chttpOpts.Header = http.Header{
  1032  		chttp.HeaderDestination: []string{targetID},
  1033  	}
  1034  
  1035  	resp, err := d.Client.DoReq(ctx, "COPY", d.path(chttp.EncodeDocID(sourceID)), chttpOpts)
  1036  	if err != nil {
  1037  		return "", err
  1038  	}
  1039  	defer chttp.CloseBody(resp.Body)
  1040  	if err := chttp.ResponseError(resp); err != nil {
  1041  		return "", err
  1042  	}
  1043  	return chttp.GetRev(resp)
  1044  }
  1045  
  1046  func (d *db) Purge(ctx context.Context, docMap map[string][]string) (*driver.PurgeResult, error) {
  1047  	result := &driver.PurgeResult{}
  1048  	options := &chttp.Options{
  1049  		GetBody: chttp.BodyEncoder(docMap),
  1050  		Header: http.Header{
  1051  			chttp.HeaderIdempotencyKey: []string{},
  1052  		},
  1053  	}
  1054  	err := d.Client.DoJSON(ctx, http.MethodPost, d.path("_purge"), options, &result)
  1055  	return result, err
  1056  }
  1057  
  1058  var _ driver.RevsDiffer = &db{}
  1059  
  1060  func (d *db) RevsDiff(ctx context.Context, revMap interface{}) (driver.Rows, error) {
  1061  	options := &chttp.Options{
  1062  		GetBody: chttp.BodyEncoder(revMap),
  1063  		Header: http.Header{
  1064  			chttp.HeaderIdempotencyKey: []string{},
  1065  		},
  1066  	}
  1067  	resp, err := d.Client.DoReq(ctx, http.MethodPost, d.path("_revs_diff"), options)
  1068  	if err != nil {
  1069  		return nil, err
  1070  	}
  1071  	if err = chttp.ResponseError(resp); err != nil {
  1072  		return nil, err
  1073  	}
  1074  	return newRevsDiffRows(ctx, resp.Body), nil
  1075  }
  1076  
  1077  type revsDiffParser struct{}
  1078  
  1079  func (p *revsDiffParser) decodeItem(i interface{}, dec *json.Decoder) error {
  1080  	t, err := dec.Token()
  1081  	if err != nil {
  1082  		return err
  1083  	}
  1084  	var value json.RawMessage
  1085  	if err := dec.Decode(&value); err != nil {
  1086  		return err
  1087  	}
  1088  	row := i.(*driver.Row)
  1089  	row.ID = t.(string)
  1090  	row.Value = bytes.NewReader(value)
  1091  	return nil
  1092  }
  1093  
  1094  func newRevsDiffRows(ctx context.Context, in io.ReadCloser) driver.Rows {
  1095  	iter := newIter(ctx, nil, "", in, &revsDiffParser{})
  1096  	iter.objMode = true
  1097  	return &rows{iter: iter}
  1098  }
  1099  
  1100  // Close is a no-op for the CouchDB driver.
  1101  func (db) Close() error { return nil }