github.com/ravendb/ravendb-go-client@v0.0.0-20240229102137-4474ee7aa0fa/batch_command.go (about) 1 package ravendb 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "mime/multipart" 8 "net/http" 9 "net/textproto" 10 "strconv" 11 "strings" 12 ) 13 14 var ( 15 _ RavenCommand = &BatchCommand{} 16 ) 17 18 // BatchCommand represents batch command 19 type BatchCommand struct { 20 RavenCommandBase 21 22 conventions *DocumentConventions 23 commands []ICommandData 24 attachmentStreams []io.Reader 25 options *BatchOptions 26 Result *JSONArrayResult 27 28 transactionMode int 29 disableAtomicDocumentWrites *bool 30 raftUniqueRequestId string 31 } 32 33 // newBatchCommand returns new BatchCommand 34 func newBatchCommand(conventions *DocumentConventions, commands []ICommandData, options *BatchOptions, transactionMode int, disableAtomicDocumentWrites *bool) (*BatchCommand, error) { 35 if conventions == nil { 36 return nil, newIllegalStateError("conventions cannot be nil") 37 } 38 if len(commands) == 0 { 39 return nil, newIllegalStateError("commands cannot be empty") 40 } 41 42 raftId, err := "", error(nil) 43 44 if transactionMode == TransactionMode_ClusterWide { 45 raftId, err = RaftId() 46 47 if err != nil { 48 return nil, err 49 } 50 } 51 52 cmd := &BatchCommand{ 53 RavenCommandBase: NewRavenCommandBase(), 54 55 commands: commands, 56 options: options, 57 conventions: conventions, 58 59 transactionMode: transactionMode, 60 disableAtomicDocumentWrites: disableAtomicDocumentWrites, 61 raftUniqueRequestId: raftId, 62 } 63 64 for i := 0; i < len(commands); i++ { 65 command := commands[i] 66 if putAttachmentCommandData, ok := command.(*PutAttachmentCommandData); ok { 67 68 stream := putAttachmentCommandData.getStream() 69 for _, existingStream := range cmd.attachmentStreams { 70 if stream == existingStream { 71 return nil, throwStreamAlready() 72 } 73 } 74 cmd.attachmentStreams = append(cmd.attachmentStreams, stream) 75 } 76 77 } 78 79 return cmd, nil 80 } 81 82 var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") 83 84 func escapeQuotes(s string) string { 85 return quoteEscaper.Replace(s) 86 } 87 88 func (c *BatchCommand) CreateRequest(node *ServerNode) (*http.Request, error) { 89 url := node.URL + "/databases/" + node.Database + "/bulk_docs" 90 url = c.appendOptions(url) 91 92 var a []interface{} 93 for _, cmd := range c.commands { 94 el, err := cmd.serialize(c.conventions) 95 if err != nil { 96 return nil, err 97 } 98 a = append(a, el) 99 } 100 101 v := map[string]interface{}{ 102 "Commands": a, 103 } 104 105 if c.transactionMode == TransactionMode_ClusterWide { 106 v["TransactionMode"] = "ClusterWide" 107 } 108 109 js, err := jsonMarshal(v) 110 if err != nil { 111 return nil, err 112 } 113 if len(c.attachmentStreams) == 0 { 114 return NewHttpPost(url, js) 115 } 116 117 body := &bytes.Buffer{} 118 writer := multipart.NewWriter(body) 119 err = writer.WriteField("main", string(js)) 120 if err != nil { 121 return nil, err 122 } 123 124 nameCounter := 1 125 for _, stream := range c.attachmentStreams { 126 name := "attachment" + strconv.Itoa(nameCounter) 127 nameCounter++ 128 h := make(textproto.MIMEHeader) 129 h.Set("Content-Disposition", 130 fmt.Sprintf(`form-data; name="%s"`, escapeQuotes(name))) 131 h.Set("Command-Type", "AttachmentStream") 132 // Note: Java seems to set those by default 133 h.Set("Content-Type", "application/octet-stream") 134 h.Set("Content-Transfer-Encoding", "binary") 135 136 part, err2 := writer.CreatePart(h) 137 if err2 != nil { 138 return nil, err2 139 } 140 _, err = io.Copy(part, stream) 141 if err != nil { 142 return nil, err 143 } 144 } 145 err = writer.Close() 146 if err != nil { 147 return nil, err 148 } 149 req, err := newHttpPostReader(url, body) 150 if err != nil { 151 return nil, err 152 } 153 contentType := writer.FormDataContentType() 154 req.Header.Set("Content-Type", contentType) 155 156 return req, nil 157 } 158 159 func (c *BatchCommand) SetResponse(response []byte, fromCache bool) error { 160 if len(response) == 0 { 161 return newIllegalStateError("Got null response from the server after doing a batch, something is very wrong. Probably a garbled response.") 162 } 163 164 return jsonUnmarshal(response, &c.Result) 165 } 166 167 func (c *BatchCommand) appendOptions(sb string) string { 168 _options := c.options 169 if _options == nil && c.transactionMode == TransactionMode_SingleNode { 170 return sb 171 } 172 173 sb += "?" 174 175 if c.transactionMode == TransactionMode_ClusterWide { 176 if c.disableAtomicDocumentWrites != nil { 177 sb += "&disableAtomicDocumentWrites=" 178 if *c.disableAtomicDocumentWrites == false { 179 sb += "false" 180 } else { 181 sb += "true" 182 } 183 } 184 185 sb += "&raft-request-id=" + c.raftUniqueRequestId 186 187 if _options == nil { 188 return sb 189 } 190 } 191 192 if _options.waitForReplicas { 193 ts := durationToTimeSpan(_options.waitForReplicasTimeout) 194 sb += "&waitForReplicasTimeout=" + ts 195 196 if _options.throwOnTimeoutInWaitForReplicas { 197 sb += "&throwOnTimeoutInWaitForReplicas=true" 198 } 199 200 sb += "&numberOfReplicasToWaitFor=" 201 if _options.majority { 202 sb += "majority" 203 } else { 204 sb += strconv.Itoa(_options.numberOfReplicasToWaitFor) 205 } 206 } 207 208 if _options.waitForIndexes { 209 ts := durationToTimeSpan(_options.waitForIndexesTimeout) 210 sb += "&waitForIndexesTimeout=" + ts 211 212 if _options.throwOnTimeoutInWaitForIndexes { 213 sb += "&waitForIndexThrow=true" 214 } else { 215 sb += "&waitForIndexThrow=false" 216 } 217 218 for _, specificIndex := range _options.waitForSpecificIndexes { 219 sb += "&waitForSpecificIndex=" + specificIndex 220 } 221 } 222 223 return sb 224 } 225 226 func (c *BatchCommand) Close() error { 227 // no-op 228 return nil 229 } 230 231 // Note: in Java is in PutAttachmentCommandHelper.java 232 func throwStreamAlready() error { 233 return newIllegalStateError("It is forbidden to re-use the same InputStream for more than one attachment. Use a unique InputStream per put attachment command.") 234 }