1
2
3
4
5
6
7
8
9
10
11
12
13 package cdb
14
15 import (
16 "context"
17 "encoding/json"
18 "errors"
19 "fmt"
20 "net/http"
21 "os"
22 "path/filepath"
23 "sort"
24 "strings"
25
26 "github.com/go-kivik/kivik/v4/driver"
27 "github.com/go-kivik/kivik/v4/x/fsdb/filesystem"
28 )
29
30
31 type Document struct {
32 ID string `json:"_id" yaml:"_id"`
33 Revisions Revisions `json:"-" yaml:"-"`
34
35
36 RevsInfo []RevInfo `json:"_revs_info,omitempty" yaml:"-"`
37
38
39 RevHistory *RevHistory `json:"_revisions,omitempty" yaml:"-"`
40
41 Options map[string]interface{} `json:"-" yaml:"-"`
42
43 cdb *FS
44 }
45
46
47 func (fs *FS) NewDocument(docID string) *Document {
48 return &Document{
49 ID: docID,
50 cdb: fs,
51 }
52 }
53
54
55 func (d *Document) MarshalJSON() ([]byte, error) {
56 d.revsInfo()
57 d.revs()
58 rev := d.Revisions[0]
59 rev.options = d.Options
60 revJSON, err := json.Marshal(rev)
61 if err != nil {
62 return nil, err
63 }
64 docJSON, _ := json.Marshal(*d)
65 return joinJSON(docJSON, revJSON), nil
66 }
67
68
69 func (d *Document) revs() {
70 d.RevHistory = nil
71 if ok, _ := d.Options["revs"].(bool); !ok {
72 return
73 }
74 if len(d.Revisions) < 1 {
75 return
76 }
77 d.RevHistory = d.Revisions[0].RevHistory
78 }
79
80
81 func (d *Document) revsInfo() {
82 d.RevsInfo = nil
83 if ok, _ := d.Options["revs_info"].(bool); !ok {
84 return
85 }
86 if _, ok := d.Options["rev"]; ok {
87 return
88 }
89 d.RevsInfo = make([]RevInfo, len(d.Revisions))
90 for i, rev := range d.Revisions {
91 d.RevsInfo[i] = RevInfo{
92 Rev: rev.Rev.String(),
93 Status: "available",
94 }
95 }
96 }
97
98
99 type RevInfo struct {
100 Rev string `json:"rev"`
101 Status string `json:"status"`
102 }
103
104
105 func (d *Document) Compact(ctx context.Context) error {
106 revTree := make(map[string]*Revision, 1)
107
108 index := map[string][]string{}
109 keep := make([]*Revision, 0, 1)
110 for _, rev := range d.Revisions {
111 revID := rev.Rev.String()
112 if leafIDs, ok := index[revID]; ok {
113 for _, leafID := range leafIDs {
114 if err := copyAttachments(d.cdb.fs, revTree[leafID], rev); err != nil {
115 return err
116 }
117 }
118 if err := rev.Delete(ctx); err != nil {
119 return err
120 }
121 continue
122 }
123 keep = append(keep, rev)
124 for _, ancestor := range rev.RevHistory.Ancestors()[1:] {
125 index[ancestor] = append(index[ancestor], revID)
126 }
127 revTree[revID] = rev
128 }
129 d.Revisions = keep
130 return nil
131 }
132
133 const tempPerms = 0o777
134
135 func copyAttachments(fs filesystem.Filesystem, leaf, old *Revision) error {
136 leafpath := strings.TrimSuffix(leaf.path, filepath.Ext(leaf.path)) + "/"
137 basepath := strings.TrimSuffix(old.path, filepath.Ext(old.path)) + "/"
138 for filename, att := range old.Attachments {
139 if _, ok := leaf.Attachments[filename]; !ok {
140 continue
141 }
142 if strings.HasPrefix(att.path, basepath) {
143 name := filepath.Base(att.path)
144 if err := os.MkdirAll(leafpath, tempPerms); err != nil {
145 return err
146 }
147 if err := fs.Link(att.path, filepath.Join(leafpath, name)); err != nil {
148 if os.IsExist(err) {
149 if err := fs.Remove(att.path); err != nil {
150 return err
151 }
152 continue
153 }
154 return err
155 }
156 }
157 }
158 return nil
159 }
160
161
162
163 func (d *Document) AddRevision(ctx context.Context, rev *Revision, options driver.Options) (string, error) {
164 revid, err := d.addRevision(ctx, rev, options)
165 if err != nil {
166 return "", err
167 }
168 err = d.persist(ctx)
169 return revid, err
170 }
171
172 func (d *Document) addOldEdit(rev *Revision) (string, error) {
173 if rev.Rev.IsZero() {
174 return "", statusError{status: http.StatusBadRequest, error: errors.New("_rev required with new_edits=false")}
175 }
176 for _, r := range d.Revisions {
177 if r.Rev.Equal(rev.Rev) {
178
179 return r.Rev.String(), nil
180 }
181 }
182 d.Revisions = append(d.Revisions, rev)
183 sort.Sort(d.Revisions)
184 return rev.Rev.String(), nil
185 }
186
187 func (d *Document) addRevision(ctx context.Context, rev *Revision, options driver.Options) (string, error) {
188 opts := map[string]interface{}{}
189 options.Apply(opts)
190 if newEdits, ok := opts["new_edits"].(bool); ok && !newEdits {
191 return d.addOldEdit(rev)
192 }
193 if revid, ok := opts["rev"].(string); ok {
194 var newrev RevID
195 if err := newrev.UnmarshalText([]byte(revid)); err != nil {
196 return "", err
197 }
198 if !rev.Rev.IsZero() && rev.Rev.String() != newrev.String() {
199 return "", statusError{status: http.StatusBadRequest, error: errors.New("document rev from request body and query string have different values")}
200 }
201 rev.Rev = newrev
202 }
203 needRev := len(d.Revisions) > 0
204 haveRev := !rev.Rev.IsZero()
205 if needRev != haveRev {
206 return "", errConflict
207 }
208 var oldrev *Revision
209 if len(d.Revisions) > 0 {
210 var ok bool
211 if oldrev, ok = d.leaves()[rev.Rev.String()]; !ok {
212 return "", errConflict
213 }
214 }
215
216 hash, err := rev.hash()
217 if err != nil {
218 return "", err
219 }
220 rev.Rev = RevID{
221 Seq: rev.Rev.Seq + 1,
222 Sum: hash,
223 }
224 if oldrev != nil {
225 rev.RevHistory = oldrev.RevHistory.AddRevision(rev.Rev)
226 }
227
228 revpath := filepath.Join(d.cdb.root, "."+EscapeID(d.ID), rev.Rev.String())
229 var dirMade bool
230 for filename, att := range rev.Attachments {
231 att.fs = d.cdb.fs
232 if err := ctx.Err(); err != nil {
233 return "", err
234 }
235 var oldatt *Attachment
236 if oldrev != nil {
237 oldatt = oldrev.Attachments[filename]
238 }
239 if !att.Stub {
240 revpos := rev.Rev.Seq
241 att.RevPos = &revpos
242 if !dirMade {
243 if err := d.cdb.fs.MkdirAll(revpath, tempPerms); err != nil && !os.IsExist(err) {
244 return "", err
245 }
246 dirMade = true
247 }
248 if err := att.persist(revpath, filename); err != nil {
249 return "", err
250 }
251 if oldatt != nil && oldatt.Digest == att.Digest {
252 if err := att.fs.Remove(att.path); err != nil {
253 return "", err
254 }
255 att.path = ""
256 att.Stub = true
257 att.RevPos = oldatt.RevPos
258 }
259 continue
260 }
261 if oldrev == nil {
262
263 return "", statusError{status: http.StatusInternalServerError, error: fmt.Errorf("attachment %s: %w", filename, err)}
264 }
265 if att.Digest != "" && att.Digest != oldatt.Digest {
266 return "", statusError{status: http.StatusBadRequest, error: fmt.Errorf("invalid attachment data for %s", filename)}
267 }
268 if att.RevPos != nil && *att.RevPos != *oldatt.RevPos {
269 return "", statusError{status: http.StatusBadRequest, error: fmt.Errorf("invalid attachment data for %s", filename)}
270 }
271 }
272
273 if len(d.Revisions) == 0 {
274 rev.RevHistory = &RevHistory{
275 Start: rev.Rev.Seq,
276 IDs: []string{rev.Rev.Sum},
277 }
278 }
279 d.Revisions = append(d.Revisions, rev)
280 sort.Sort(d.Revisions)
281 return rev.Rev.String(), nil
282 }
283
284
294 func (d *Document) persist(ctx context.Context) error {
295 if d == nil || len(d.Revisions) == 0 {
296 return statusError{status: http.StatusBadRequest, error: errors.New("document has no revisions")}
297 }
298 docID := EscapeID(d.ID)
299 for _, rev := range d.Revisions {
300 if rev.path != "" {
301 continue
302 }
303 if err := ctx.Err(); err != nil {
304 return err
305 }
306 if err := rev.persist(ctx, filepath.Join(d.cdb.root, "."+docID, rev.Rev.String())); err != nil {
307 return err
308 }
309 }
310
311
312 sort.Sort(d.Revisions)
313
314 winningRev := d.Revisions[0]
315 winningPath := filepath.Join(d.cdb.root, docID)
316 if winningPath+filepath.Ext(winningRev.path) == winningRev.path {
317
318 return nil
319 }
320
321
322 for _, rev := range d.Revisions[1:] {
323 if winningPath+filepath.Ext(rev.path) == rev.path {
324 if err := ctx.Err(); err != nil {
325 return err
326 }
327
328 revpath := filepath.Join(d.cdb.root, "."+EscapeID(d.ID), rev.Rev.String())
329 if err := d.cdb.fs.Mkdir(revpath, tempPerms); err != nil && !os.IsExist(err) {
330 return err
331 }
332
333 for attname, att := range rev.Attachments {
334 if !strings.HasPrefix(att.path, rev.path+"/") {
335
336 continue
337 }
338 newpath := filepath.Join(revpath, attname)
339 if err := d.cdb.fs.Rename(att.path, newpath); err != nil {
340 return err
341 }
342 att.path = newpath
343 }
344
345 _ = d.cdb.fs.Remove(rev.path + "/")
346
347 if err := d.cdb.fs.Rename(rev.path, revpath+filepath.Ext(rev.path)); err != nil {
348 return err
349 }
350
351 _ = d.cdb.fs.Remove(filepath.Dir(rev.path))
352 break
353 }
354 }
355
356
357 if err := d.cdb.fs.Rename(winningRev.path, winningPath+filepath.Ext(winningRev.path)); err != nil {
358 return err
359 }
360 winningRev.path = winningPath + filepath.Ext(winningRev.path)
361
362 if err := d.cdb.fs.Mkdir(winningPath, tempPerms); err != nil && !os.IsExist(err) {
363 return err
364 }
365 revpath := filepath.Join(d.cdb.root, "."+EscapeID(d.ID), winningRev.Rev.String()) + "/"
366 for attname, att := range winningRev.Attachments {
367 if !strings.HasPrefix(att.path, revpath) {
368
369 continue
370 }
371 if err := ctx.Err(); err != nil {
372 return err
373 }
374 newpath := filepath.Join(winningPath, attname)
375 if err := d.cdb.fs.Rename(att.path, newpath); err != nil {
376 return err
377 }
378 att.path = newpath
379 }
380
381 _ = d.cdb.fs.Remove(filepath.Dir(revpath))
382 _ = d.cdb.fs.Remove(filepath.Dir(filepath.Dir(revpath)))
383
384 return nil
385 }
386
387
388 func (d *Document) leaves() map[string]*Revision {
389 if len(d.Revisions) == 1 {
390 return map[string]*Revision{
391 d.Revisions[0].Rev.String(): d.Revisions[0],
392 }
393 }
394 leaves := make(map[string]*Revision, len(d.Revisions))
395 for _, rev := range d.Revisions {
396 leaves[rev.Rev.String()] = rev
397 }
398 for _, rev := range d.Revisions {
399
400 for _, revid := range rev.RevHistory.Ancestors()[1:] {
401 delete(leaves, revid)
402 }
403 }
404 return leaves
405 }
406
View as plain text