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