github.com/altipla-consulting/ravendb-go-client@v0.1.3/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 27 Result *JSONArrayResult 28 } 29 30 // newBatchCommand returns new BatchCommand 31 func newBatchCommand(conventions *DocumentConventions, commands []ICommandData, options *BatchOptions) (*BatchCommand, error) { 32 if conventions == nil { 33 return nil, newIllegalStateError("conventions cannot be nil") 34 } 35 if len(commands) == 0 { 36 return nil, newIllegalStateError("commands cannot be empty") 37 } 38 39 cmd := &BatchCommand{ 40 RavenCommandBase: NewRavenCommandBase(), 41 42 commands: commands, 43 options: options, 44 conventions: conventions, 45 } 46 47 for i := 0; i < len(commands); i++ { 48 command := commands[i] 49 if putAttachmentCommandData, ok := command.(*PutAttachmentCommandData); ok { 50 51 stream := putAttachmentCommandData.getStream() 52 for _, existingStream := range cmd.attachmentStreams { 53 if stream == existingStream { 54 return nil, throwStreamAlready() 55 } 56 } 57 cmd.attachmentStreams = append(cmd.attachmentStreams, stream) 58 } 59 } 60 61 return cmd, nil 62 } 63 64 var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") 65 66 func escapeQuotes(s string) string { 67 return quoteEscaper.Replace(s) 68 } 69 70 func (c *BatchCommand) createRequest(node *ServerNode) (*http.Request, error) { 71 url := node.URL + "/databases/" + node.Database + "/bulk_docs" 72 url = c.appendOptions(url) 73 74 var a []interface{} 75 for _, cmd := range c.commands { 76 el, err := cmd.serialize(c.conventions) 77 if err != nil { 78 return nil, err 79 } 80 a = append(a, el) 81 } 82 v := map[string]interface{}{ 83 "Commands": a, 84 } 85 js, err := jsonMarshal(v) 86 if err != nil { 87 return nil, err 88 } 89 if len(c.attachmentStreams) == 0 { 90 return newHttpPost(url, js) 91 } 92 93 body := &bytes.Buffer{} 94 writer := multipart.NewWriter(body) 95 err = writer.WriteField("main", string(js)) 96 if err != nil { 97 return nil, err 98 } 99 100 nameCounter := 1 101 for _, stream := range c.attachmentStreams { 102 name := "attachment" + strconv.Itoa(nameCounter) 103 nameCounter++ 104 h := make(textproto.MIMEHeader) 105 h.Set("Content-Disposition", 106 fmt.Sprintf(`form-data; name="%s"`, escapeQuotes(name))) 107 h.Set("Command-Type", "AttachmentStream") 108 // Note: Java seems to set those by default 109 h.Set("Content-Type", "application/octet-stream") 110 h.Set("Content-Transfer-Encoding", "binary") 111 112 part, err2 := writer.CreatePart(h) 113 if err2 != nil { 114 return nil, err2 115 } 116 _, err = io.Copy(part, stream) 117 if err != nil { 118 return nil, err 119 } 120 } 121 err = writer.Close() 122 if err != nil { 123 return nil, err 124 } 125 req, err := newHttpPostReader(url, body) 126 if err != nil { 127 return nil, err 128 } 129 contentType := writer.FormDataContentType() 130 req.Header.Set("Content-Type", contentType) 131 132 return req, nil 133 } 134 135 func (c *BatchCommand) setResponse(response []byte, fromCache bool) error { 136 if len(response) == 0 { 137 return newIllegalStateError("Got null response from the server after doing a batch, something is very wrong. Probably a garbled response.") 138 } 139 140 return jsonUnmarshal(response, &c.Result) 141 } 142 143 func (c *BatchCommand) appendOptions(sb string) string { 144 _options := c.options 145 if _options == nil { 146 return sb 147 } 148 149 sb += "?" 150 151 if _options.waitForReplicas { 152 ts := durationToTimeSpan(_options.waitForReplicasTimeout) 153 sb += "&waitForReplicasTimeout=" + ts 154 155 if _options.throwOnTimeoutInWaitForReplicas { 156 sb += "&throwOnTimeoutInWaitForReplicas=true" 157 } 158 159 sb += "&numberOfReplicasToWaitFor=" 160 if _options.majority { 161 sb += "majority" 162 } else { 163 sb += strconv.Itoa(_options.numberOfReplicasToWaitFor) 164 } 165 } 166 167 if _options.waitForIndexes { 168 ts := durationToTimeSpan(_options.waitForIndexesTimeout) 169 sb += "&waitForIndexesTimeout=" + ts 170 171 if _options.throwOnTimeoutInWaitForIndexes { 172 sb += "&waitForIndexThrow=true" 173 } else { 174 sb += "&waitForIndexThrow=false" 175 } 176 177 for _, specificIndex := range _options.waitForSpecificIndexes { 178 sb += "&waitForSpecificIndex=" + specificIndex 179 } 180 } 181 return sb 182 } 183 184 func (c *BatchCommand) Close() error { 185 // no-op 186 return nil 187 } 188 189 // Note: in Java is in PutAttachmentCommandHelper.java 190 func throwStreamAlready() error { 191 return newIllegalStateError("It is forbidden to re-use the same InputStream for more than one attachment. Use a unique InputStream per put attachment command.") 192 }