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  }