...

Source file src/github.com/go-kivik/kivik/v4/x/fsdb/cdb/attachments.go

Documentation: github.com/go-kivik/kivik/v4/x/fsdb/cdb

     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 cdb
    14  
    15  import (
    16  	"bytes"
    17  	"encoding/json"
    18  	"errors"
    19  	"io"
    20  	"path/filepath"
    21  
    22  	"github.com/go-kivik/kivik/v4/driver"
    23  	"github.com/go-kivik/kivik/v4/x/fsdb/filesystem"
    24  )
    25  
    26  /*
    27  When uploading attachment stubs:
    28  - revpos must match, or be omitted
    29  - digest must match
    30  - length is ignored
    31  - content_type is ignored
    32  */
    33  
    34  // Attachment represents a file attachment.
    35  type Attachment struct {
    36  	ContentType string `json:"content_type" yaml:"content_type"`
    37  	RevPos      *int64 `json:"revpos,omitempty" yaml:"revpos,omitempty"`
    38  	Stub        bool   `json:"stub,omitempty" yaml:"stub,omitempty"`
    39  	Follows     bool   `json:"follows,omitempty" yaml:"follows,omitempty"`
    40  	Content     []byte `json:"data,omitempty" yaml:"content,omitempty"`
    41  	Size        int64  `json:"length" yaml:"size"`
    42  	Digest      string `json:"digest" yaml:"digest"`
    43  
    44  	// path is the full path to the file on disk, or the empty string if the
    45  	// attachment is not (yet) on disk.
    46  	path string
    47  	// fs is the filesystem to use for disk access.
    48  	fs filesystem.Filesystem
    49  
    50  	// outputStub dictates whether MarshalJSON should output a stub. This is
    51  	// distinct from Stub, which indicates whether UnmarshalJSON read Stub, as
    52  	// from user input.
    53  	outputStub bool
    54  }
    55  
    56  // Open opens the attachment for reading.
    57  func (a *Attachment) Open() (filesystem.File, error) {
    58  	if a.path == "" {
    59  		return nil, errors.New("no path defined")
    60  	}
    61  	return a.fs.Open(a.path)
    62  }
    63  
    64  // MarshalJSON implements the json.Marshaler interface.
    65  func (a *Attachment) MarshalJSON() ([]byte, error) {
    66  	var err error
    67  	switch {
    68  	case len(a.Content) != 0:
    69  		a.setMetadata()
    70  	case a.outputStub || a.Follows:
    71  		err = a.readMetadata()
    72  	default:
    73  		err = a.readContent()
    74  	}
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	att := struct {
    79  		Attachment
    80  		Content *[]byte `json:"data,omitempty"`    // nolint: govet
    81  		Stub    *bool   `json:"stub,omitempty"`    // nolint: govet
    82  		Follows *bool   `json:"follows,omitempty"` // nolint: govet
    83  	}{
    84  		Attachment: *a,
    85  	}
    86  	switch {
    87  	case a.outputStub:
    88  		att.Stub = &a.outputStub
    89  	case a.Follows:
    90  		att.Follows = &a.Follows
    91  	case len(a.Content) > 0:
    92  		att.Content = &a.Content
    93  	}
    94  	return json.Marshal(att)
    95  }
    96  
    97  func (a *Attachment) readContent() error {
    98  	f, err := a.fs.Open(a.path)
    99  	if err != nil {
   100  		return err
   101  	}
   102  	buf := &bytes.Buffer{}
   103  	a.Size, a.Digest, err = copyDigest(buf, f)
   104  	if err != nil {
   105  		return err
   106  	}
   107  	a.Content = buf.Bytes()
   108  	return nil
   109  }
   110  
   111  func (a *Attachment) readMetadata() error {
   112  	if a.path == "" {
   113  		return nil
   114  	}
   115  	f, err := a.fs.Open(a.path)
   116  	if err != nil {
   117  		return err
   118  	}
   119  	a.Size, a.Digest = digest(f)
   120  	return nil
   121  }
   122  
   123  func (a *Attachment) setMetadata() {
   124  	a.Size, a.Digest = digest(bytes.NewReader(a.Content))
   125  }
   126  
   127  func (a *Attachment) persist(path, attname string) error {
   128  	target := filepath.Join(path, attname)
   129  	if err := atomicWriteFile(a.fs, target, bytes.NewReader(a.Content)); err != nil {
   130  		return err
   131  	}
   132  	a.Content = nil
   133  	a.path = target
   134  	return nil
   135  }
   136  
   137  type attsIter []*driver.Attachment
   138  
   139  var _ driver.Attachments = &attsIter{}
   140  
   141  func (i attsIter) Close() error {
   142  	for _, att := range i {
   143  		if err := att.Content.Close(); err != nil {
   144  			return err
   145  		}
   146  	}
   147  	return nil
   148  }
   149  
   150  func (i *attsIter) Next(att *driver.Attachment) error {
   151  	if len(*i) == 0 {
   152  		return io.EOF
   153  	}
   154  	var next *driver.Attachment
   155  	next, *i = (*i)[0], (*i)[1:]
   156  	*att = *next
   157  	return nil
   158  }
   159  
   160  // AttachmentsIterator will return a driver.Attachments iterator, if the options
   161  // permit. If options don't permit, both return values will be nil.
   162  func (r *Revision) AttachmentsIterator() (driver.Attachments, error) {
   163  	if attachments, _ := r.options["attachments"].(bool); !attachments {
   164  		return nil, nil
   165  	}
   166  	if accept, _ := r.options["header:accept"].(string); accept == "application/json" {
   167  		return nil, nil
   168  	}
   169  	iter := make(attsIter, 0, len(r.Attachments))
   170  	for filename, att := range r.Attachments {
   171  		f, err := att.Open()
   172  		if err != nil {
   173  			return nil, err
   174  		}
   175  		drAtt := &driver.Attachment{
   176  			Filename:    filename,
   177  			Content:     f,
   178  			ContentType: att.ContentType,
   179  			Stub:        att.Stub,
   180  			Follows:     att.Follows,
   181  			Size:        att.Size,
   182  			Digest:      att.Digest,
   183  		}
   184  		if att.RevPos != nil {
   185  			drAtt.RevPos = *att.RevPos
   186  		}
   187  		iter = append(iter, drAtt)
   188  	}
   189  	return &iter, nil
   190  }
   191  

View as plain text