github.com/go-kivik/kivik/v4@v4.3.2/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 kivik 14 15 import ( 16 "bytes" 17 "encoding/json" 18 "io" 19 20 "github.com/go-kivik/kivik/v4/driver" 21 ) 22 23 // Attachments is a collection of one or more file attachments. 24 type Attachments map[string]*Attachment 25 26 // Get fetches the requested attachment, or returns nil if it does not exist. 27 func (a *Attachments) Get(filename string) *Attachment { 28 return map[string]*Attachment(*a)[filename] 29 } 30 31 // Set sets the attachment associated with filename in the collection, 32 // replacing it if it already exists. 33 func (a *Attachments) Set(filename string, att *Attachment) { 34 map[string]*Attachment(*a)[filename] = att 35 } 36 37 // Delete removes the specified file from the collection. 38 func (a *Attachments) Delete(filename string) { 39 delete(map[string]*Attachment(*a), filename) 40 } 41 42 // Attachment represents a file attachment on a CouchDB document. 43 type Attachment struct { 44 // Filename is the name of the attachment. 45 Filename string `json:"-"` 46 47 // ContentType is the Content-Type type of the attachment. 48 ContentType string `json:"content_type"` 49 50 // Stub will be true if the data structure only represents file metadata, 51 // and contains no actual content. Stub will be true when returned by 52 // [DB.GetAttachmentMeta], or when included in a document without the 53 // 'include_docs' option. 54 Stub bool `json:"stub"` 55 56 // Follows will be true when reading attachments in multipart/related 57 // format. 58 Follows bool `json:"follows"` 59 60 // Content represents the attachment's content. 61 // 62 // Kivik will always return a non-nil Content, even for 0-byte attachments 63 // or when Stub is true. It is the caller's responsibility to close 64 // Content. 65 Content io.ReadCloser `json:"-"` 66 67 // Size records the uncompressed size of the attachment. The value -1 68 // indicates that the length is unknown. Unless [Attachment.Stub] is true, 69 // values >= 0 indicate that the given number of bytes may be read from 70 // [Attachment.Content]. 71 Size int64 `json:"length"` 72 73 // Used compression codec, if any. Will be the empty string if the 74 // attachment is uncompressed. 75 ContentEncoding string `json:"encoding"` 76 77 // EncodedLength records the compressed attachment size in bytes. Only 78 // meaningful when [Attachment.ContentEncoding] is defined. 79 EncodedLength int64 `json:"encoded_length"` 80 81 // RevPos is the revision number when attachment was added. 82 RevPos int64 `json:"revpos"` 83 84 // Digest is the content hash digest. 85 Digest string `json:"digest"` 86 } 87 88 // bufCloser wraps a *bytes.Buffer to create an io.ReadCloser 89 type bufCloser struct { 90 *bytes.Buffer 91 } 92 93 var _ io.ReadCloser = &bufCloser{} 94 95 func (b *bufCloser) Close() error { return nil } 96 97 // validate returns an error if the attachment is invalid. 98 func (a *Attachment) validate() error { 99 if a == nil { 100 return missingArg("attachment") 101 } 102 if a.Filename == "" { 103 return missingArg("filename") 104 } 105 return nil 106 } 107 108 // MarshalJSON satisfies the [encoding/json.Marshaler] interface. 109 func (a *Attachment) MarshalJSON() ([]byte, error) { 110 type jsonAttachment struct { 111 ContentType string `json:"content_type"` 112 Stub *bool `json:"stub,omitempty"` 113 Follows *bool `json:"follows,omitempty"` 114 Size int64 `json:"length,omitempty"` 115 RevPos int64 `json:"revpos,omitempty"` 116 Data []byte `json:"data,omitempty"` 117 Digest string `json:"digest,omitempty"` 118 } 119 att := &jsonAttachment{ 120 ContentType: a.ContentType, 121 Size: a.Size, 122 RevPos: a.RevPos, 123 Digest: a.Digest, 124 } 125 switch { 126 case a.Stub: 127 att.Stub = &a.Stub 128 case a.Follows: 129 att.Follows = &a.Follows 130 default: 131 defer a.Content.Close() // nolint: errcheck 132 data, err := io.ReadAll(a.Content) 133 if err != nil { 134 return nil, err 135 } 136 att.Data = data 137 } 138 return json.Marshal(att) 139 } 140 141 // UnmarshalJSON implements the [encoding/json.Unmarshaler] interface. 142 func (a *Attachment) UnmarshalJSON(data []byte) error { 143 type clone Attachment 144 type jsonAtt struct { 145 clone 146 Data []byte `json:"data"` 147 } 148 var att jsonAtt 149 if err := json.Unmarshal(data, &att); err != nil { 150 return err 151 } 152 *a = Attachment(att.clone) 153 if att.Data != nil { 154 a.Content = io.NopCloser(bytes.NewReader(att.Data)) 155 } else { 156 a.Content = nilContent 157 } 158 return nil 159 } 160 161 // UnmarshalJSON implements the [encoding/json.Unmarshaler] interface. 162 func (a *Attachments) UnmarshalJSON(data []byte) error { 163 atts := make(map[string]*Attachment) 164 if err := json.Unmarshal(data, &atts); err != nil { 165 return err 166 } 167 for filename, att := range atts { 168 att.Filename = filename 169 } 170 *a = atts 171 return nil 172 } 173 174 // AttachmentsIterator allows reading streamed attachments from a multi-part 175 // [DB.Get] request. 176 type AttachmentsIterator struct { 177 atti driver.Attachments 178 onClose func() 179 } 180 181 // Next returns the next attachment in the stream. [io.EOF] will be 182 // returned when there are no more attachments. 183 // 184 // The returned attachment is only valid until the next call to [Next], or a 185 // call to [Close]. 186 func (i *AttachmentsIterator) Next() (*Attachment, error) { 187 att := new(driver.Attachment) 188 if err := i.atti.Next(att); err != nil { 189 if err == io.EOF { 190 if e2 := i.Close(); e2 != nil { 191 return nil, e2 192 } 193 } 194 return nil, err 195 } 196 katt := Attachment(*att) 197 return &katt, nil 198 } 199 200 // Close closes the AttachmentsIterator. It is automatically called when 201 // [AttachmentsIterator.Next] returns [io.EOF]. 202 func (i *AttachmentsIterator) Close() error { 203 if i.onClose != nil { 204 i.onClose() 205 } 206 return i.atti.Close() 207 } 208 209 // Iterator returns a function that can be used to iterate over the attachments. 210 // This function works with Go 1.23's range functions, and is an alternative to 211 // using [AttachmentsIterator.Next] directly. 212 func (i *AttachmentsIterator) Iterator() func(yield func(*Attachment, error) bool) { 213 return func(yield func(*Attachment, error) bool) { 214 for { 215 att, err := i.Next() 216 if err == io.EOF { 217 return 218 } 219 if !yield(att, err) || err != nil { 220 _ = i.Close() 221 return 222 } 223 } 224 } 225 }