github.com/go-kivik/kivik/v4@v4.3.2/x/fsdb/cdb/revision.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 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 }