...

Source file src/github.com/go-kivik/kivik/v4/x/fsdb/cdb/revision.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  	"context"
    17  	"crypto/md5"
    18  	"encoding/json"
    19  	"fmt"
    20  	"net/http"
    21  	"os"
    22  	"path/filepath"
    23  	"sort"
    24  	"strings"
    25  
    26  	"github.com/icza/dyno"
    27  
    28  	"github.com/go-kivik/kivik/v4/x/fsdb/filesystem"
    29  )
    30  
    31  // RevMeta is the metadata stored in reach revision.
    32  type RevMeta struct {
    33  	Rev         RevID                  `json:"_rev" yaml:"_rev"`
    34  	Deleted     *bool                  `json:"_deleted,omitempty" yaml:"_deleted,omitempty"`
    35  	Attachments map[string]*Attachment `json:"_attachments,omitempty" yaml:"_attachments,omitempty"`
    36  	RevHistory  *RevHistory            `json:"_revisions,omitempty" yaml:"_revisions,omitempty"`
    37  
    38  	// isMain should be set to true when unmarshaling the main Rev, to enable
    39  	// auto-population of the _rev key, if necessary
    40  	isMain bool                  // nolint: structcheck
    41  	path   string                // nolint: structcheck
    42  	fs     filesystem.Filesystem // nolint: structcheck
    43  }
    44  
    45  // Revision is a specific instance of a document.
    46  type Revision struct {
    47  	RevMeta
    48  
    49  	// Data is the normal payload
    50  	Data map[string]interface{} `json:"-" yaml:"-"`
    51  
    52  	options map[string]interface{}
    53  }
    54  
    55  // UnmarshalJSON satisfies the json.Unmarshaler interface.
    56  func (r *Revision) UnmarshalJSON(p []byte) error {
    57  	if err := json.Unmarshal(p, &r.RevMeta); err != nil {
    58  		return err
    59  	}
    60  	if err := json.Unmarshal(p, &r.Data); err != nil {
    61  		return err
    62  	}
    63  	return r.finalizeUnmarshal()
    64  }
    65  
    66  // UnmarshalYAML satisfies the yaml.Unmarshaler interface.
    67  func (r *Revision) UnmarshalYAML(u func(interface{}) error) error {
    68  	if err := u(&r.RevMeta); err != nil {
    69  		return err
    70  	}
    71  	if err := u(&r.Data); err != nil {
    72  		return err
    73  	}
    74  	r.Data = dyno.ConvertMapI2MapS(r.Data).(map[string]interface{})
    75  	return r.finalizeUnmarshal()
    76  }
    77  
    78  func (r *Revision) finalizeUnmarshal() error {
    79  	for key := range reservedKeys {
    80  		delete(r.Data, key)
    81  	}
    82  	if r.isMain && r.Rev.IsZero() {
    83  		r.Rev = RevID{Seq: 1}
    84  	}
    85  	if !r.isMain && r.path != "" {
    86  		revstr := filepath.Base(strings.TrimSuffix(r.path, filepath.Ext(r.path)))
    87  		if err := r.Rev.UnmarshalText([]byte(revstr)); err != nil {
    88  			return errUnrecognizedFile
    89  		}
    90  	}
    91  	if r.RevHistory == nil {
    92  		var ids []string
    93  		if r.Rev.Sum == "" {
    94  			histSize := r.Rev.Seq
    95  			if histSize > revsLimit {
    96  				histSize = revsLimit
    97  			}
    98  			ids = make([]string, int(histSize))
    99  		} else {
   100  			ids = []string{r.Rev.Sum}
   101  		}
   102  		r.RevHistory = &RevHistory{
   103  			Start: r.Rev.Seq,
   104  			IDs:   ids,
   105  		}
   106  	}
   107  	return nil
   108  }
   109  
   110  // MarshalJSON satisfies the json.Marshaler interface
   111  func (r *Revision) MarshalJSON() ([]byte, error) {
   112  	var meta interface{} = r.RevMeta
   113  	revs, _ := r.options["revs"].(bool)
   114  	if _, ok := r.options["rev"]; ok {
   115  		revs = false
   116  	}
   117  	if !revs {
   118  		meta = struct {
   119  			RevMeta
   120  			// This suppresses RevHistory from being included in the default output
   121  			RevHistory *RevHistory `json:"_revisions,omitempty"` // nolint: govet
   122  		}{
   123  			RevMeta: r.RevMeta,
   124  		}
   125  	}
   126  	stub, follows := r.stubFollows()
   127  	for _, att := range r.Attachments {
   128  		att.outputStub = stub
   129  		att.Follows = follows
   130  	}
   131  	const maxParts = 2
   132  	parts := make([]json.RawMessage, 0, maxParts)
   133  	metaJSON, err := json.Marshal(meta)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	parts = append(parts, metaJSON)
   138  	if len(r.Data) > 0 {
   139  		dataJSON, err := json.Marshal(r.Data)
   140  		if err != nil {
   141  			return nil, err
   142  		}
   143  		parts = append(parts, dataJSON)
   144  	}
   145  	return joinJSON(parts...), nil
   146  }
   147  
   148  func (r *Revision) stubFollows() (bool, bool) {
   149  	attachments, _ := r.options["attachments"].(bool)
   150  	if !attachments {
   151  		return true, false
   152  	}
   153  	accept, _ := r.options["header:accept"].(string)
   154  	return false, accept != "application/json"
   155  }
   156  
   157  func (r *Revision) openAttachment(filename string) (filesystem.File, error) {
   158  	path := strings.TrimSuffix(r.path, filepath.Ext(r.path))
   159  	f, err := r.fs.Open(filepath.Join(path, filename))
   160  	if !os.IsNotExist(err) {
   161  		return f, err
   162  	}
   163  	basename := filepath.Base(path)
   164  	path = strings.TrimSuffix(path, basename)
   165  	if basename != r.Rev.String() {
   166  		// We're working with the main rev
   167  		path += "." + basename
   168  	}
   169  	for _, rev := range r.RevHistory.Ancestors() {
   170  		fullpath := filepath.Join(path, rev, filename)
   171  		f, err := r.fs.Open(fullpath)
   172  		if !os.IsNotExist(err) {
   173  			return f, err
   174  		}
   175  	}
   176  	return nil, fmt.Errorf("attachment '%s': %w", filename, errNotFound)
   177  }
   178  
   179  // Revisions is a sortable list of document revisions.
   180  type Revisions []*Revision
   181  
   182  var _ sort.Interface = Revisions{}
   183  
   184  // Len returns the number of elements in r.
   185  func (r Revisions) Len() int {
   186  	return len(r)
   187  }
   188  
   189  func (r Revisions) Less(i, j int) bool {
   190  	return r[i].Rev.Seq > r[j].Rev.Seq ||
   191  		(r[i].Rev.Seq == r[j].Rev.Seq && r[i].Rev.Sum > r[j].Rev.Sum)
   192  }
   193  
   194  func (r Revisions) Swap(i, j int) {
   195  	r[i], r[j] = r[j], r[i]
   196  }
   197  
   198  // Deleted returns true if the winning revision is deleted.
   199  func (r Revisions) Deleted() bool {
   200  	if len(r) < 1 {
   201  		return true
   202  	}
   203  	deleted := r[0].Deleted
   204  	return deleted != nil && *deleted
   205  }
   206  
   207  // Delete deletes the revision.
   208  func (r *Revision) Delete(context.Context) error {
   209  	if err := os.Remove(r.path); err != nil {
   210  		return err
   211  	}
   212  	attpath := strings.TrimSuffix(r.path, filepath.Ext(r.path))
   213  	return os.RemoveAll(attpath)
   214  }
   215  
   216  // NewRevision creates a new revision from i, according to opts.
   217  func (fs *FS) NewRevision(i interface{}) (*Revision, error) {
   218  	data, err := json.Marshal(i)
   219  	if err != nil {
   220  		return nil, statusError{status: http.StatusBadRequest, error: err}
   221  	}
   222  	rev := new(Revision)
   223  	rev.fs = fs.fs
   224  	if err := json.Unmarshal(data, &rev); err != nil {
   225  		return nil, statusError{status: http.StatusBadRequest, error: err}
   226  	}
   227  	for _, att := range rev.Attachments {
   228  		if att.RevPos == nil {
   229  			revpos := rev.Rev.Seq
   230  			att.RevPos = &revpos
   231  		}
   232  	}
   233  	return rev, nil
   234  }
   235  
   236  func (r *Revision) persist(ctx context.Context, path string) error {
   237  	if err := r.fs.Mkdir(filepath.Dir(path), tempPerms); err != nil && !os.IsExist(err) {
   238  		return err
   239  	}
   240  	var dirMade bool
   241  	for attname, att := range r.Attachments {
   242  		if att.Stub || att.path != "" {
   243  			continue
   244  		}
   245  		if err := ctx.Err(); err != nil {
   246  			return err
   247  		}
   248  		if !dirMade {
   249  			if err := r.fs.Mkdir(path, tempPerms); err != nil && !os.IsExist(err) {
   250  				return err
   251  			}
   252  			dirMade = true
   253  		}
   254  		att.fs = r.fs
   255  		if err := att.persist(path, attname); err != nil {
   256  			return err
   257  		}
   258  	}
   259  	f := atomicFileWriter(r.fs, path+".json")
   260  	defer f.Close() // nolint: errcheck
   261  	r.options = map[string]interface{}{"revs": true}
   262  	if err := json.NewEncoder(f).Encode(r); err != nil {
   263  		return err
   264  	}
   265  	if err := f.Close(); err != nil {
   266  		return err
   267  	}
   268  	r.path = path + ".json"
   269  	return nil
   270  }
   271  
   272  // hash passes deterministic JSON content of the revision through md5 to
   273  // generate a hash to be used in the revision ID.
   274  func (r *Revision) hash() (string, error) {
   275  	r.options = nil
   276  	data, err := json.Marshal(r)
   277  	if err != nil {
   278  		return "", err
   279  	}
   280  	h := md5.New()
   281  	_, _ = h.Write(data)
   282  	return fmt.Sprintf("%x", h.Sum(nil)), nil
   283  }
   284  

View as plain text