1
2
3
4
5
6
7
8
9
10
11
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
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
39
40 isMain bool
41 path string
42 fs filesystem.Filesystem
43 }
44
45
46 type Revision struct {
47 RevMeta
48
49
50 Data map[string]interface{} `json:"-" yaml:"-"`
51
52 options map[string]interface{}
53 }
54
55
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
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
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
121 RevHistory *RevHistory `json:"_revisions,omitempty"`
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
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
180 type Revisions []*Revision
181
182 var _ sort.Interface = Revisions{}
183
184
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
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
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
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()
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
273
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