// Copyright 2016 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package gensupport import ( "fmt" "io" "io/ioutil" "mime/multipart" "net/http" "net/textproto" "google.golang.org/api/googleapi" ) const sniffBuffSize = 512 func newContentSniffer(r io.Reader) *contentSniffer { return &contentSniffer{r: r} } // contentSniffer wraps a Reader, and reports the content type determined by sniffing up to 512 bytes from the Reader. type contentSniffer struct { r io.Reader start []byte // buffer for the sniffed bytes. err error // set to any error encountered while reading bytes to be sniffed. ctype string // set on first sniff. sniffed bool // set to true on first sniff. } func (cs *contentSniffer) Read(p []byte) (n int, err error) { // Ensure that the content type is sniffed before any data is consumed from Reader. _, _ = cs.ContentType() if len(cs.start) > 0 { n := copy(p, cs.start) cs.start = cs.start[n:] return n, nil } // We may have read some bytes into start while sniffing, even if the read ended in an error. // We should first return those bytes, then the error. if cs.err != nil { return 0, cs.err } // Now we have handled all bytes that were buffered while sniffing. Now just delegate to the underlying reader. return cs.r.Read(p) } // ContentType returns the sniffed content type, and whether the content type was succesfully sniffed. func (cs *contentSniffer) ContentType() (string, bool) { if cs.sniffed { return cs.ctype, cs.ctype != "" } cs.sniffed = true // If ReadAll hits EOF, it returns err==nil. cs.start, cs.err = ioutil.ReadAll(io.LimitReader(cs.r, sniffBuffSize)) // Don't try to detect the content type based on possibly incomplete data. if cs.err != nil { return "", false } cs.ctype = http.DetectContentType(cs.start) return cs.ctype, true } // DetermineContentType determines the content type of the supplied reader. // If the content type is already known, it can be specified via ctype. // Otherwise, the content of media will be sniffed to determine the content type. // If media implements googleapi.ContentTyper (deprecated), this will be used // instead of sniffing the content. // After calling DetectContentType the caller must not perform further reads on // media, but rather read from the Reader that is returned. func DetermineContentType(media io.Reader, ctype string) (io.Reader, string) { // Note: callers could avoid calling DetectContentType if ctype != "", // but doing the check inside this function reduces the amount of // generated code. if ctype != "" { return media, ctype } // For backwards compatability, allow clients to set content // type by providing a ContentTyper for media. if typer, ok := media.(googleapi.ContentTyper); ok { return media, typer.ContentType() } sniffer := newContentSniffer(media) if ctype, ok := sniffer.ContentType(); ok { return sniffer, ctype } // If content type could not be sniffed, reads from sniffer will eventually fail with an error. return sniffer, "" } type typeReader struct { io.Reader typ string } // multipartReader combines the contents of multiple readers to creat a multipart/related HTTP body. // Close must be called if reads from the multipartReader are abandoned before reaching EOF. type multipartReader struct { pr *io.PipeReader pipeOpen bool ctype string } func newMultipartReader(parts []typeReader) *multipartReader { mp := &multipartReader{pipeOpen: true} var pw *io.PipeWriter mp.pr, pw = io.Pipe() mpw := multipart.NewWriter(pw) mp.ctype = "multipart/related; boundary=" + mpw.Boundary() go func() { for _, part := range parts { w, err := mpw.CreatePart(typeHeader(part.typ)) if err != nil { mpw.Close() pw.CloseWithError(fmt.Errorf("googleapi: CreatePart failed: %v", err)) return } _, err = io.Copy(w, part.Reader) if err != nil { mpw.Close() pw.CloseWithError(fmt.Errorf("googleapi: Copy failed: %v", err)) return } } mpw.Close() pw.Close() }() return mp } func (mp *multipartReader) Read(data []byte) (n int, err error) { return mp.pr.Read(data) } func (mp *multipartReader) Close() error { if !mp.pipeOpen { return nil } mp.pipeOpen = false return mp.pr.Close() } // CombineBodyMedia combines a json body with media content to create a multipart/related HTTP body. // It returns a ReadCloser containing the combined body, and the overall "multipart/related" content type, with random boundary. // // The caller must call Close on the returned ReadCloser if reads are abandoned before reaching EOF. func CombineBodyMedia(body io.Reader, bodyContentType string, media io.Reader, mediaContentType string) (io.ReadCloser, string) { mp := newMultipartReader([]typeReader{ {body, bodyContentType}, {media, mediaContentType}, }) return mp, mp.ctype } func typeHeader(contentType string) textproto.MIMEHeader { h := make(textproto.MIMEHeader) if contentType != "" { h.Set("Content-Type", contentType) } return h } // PrepareUpload determines whether the data in the supplied reader should be // uploaded in a single request, or in sequential chunks. // chunkSize is the size of the chunk that media should be split into. // If chunkSize is non-zero and the contents of media do not fit in a single // chunk (or there is an error reading media), then media will be returned as a // MediaBuffer. Otherwise, media will be returned as a Reader. // // After PrepareUpload has been called, media should no longer be used: the // media content should be accessed via one of the return values. func PrepareUpload(media io.Reader, chunkSize int) (io.Reader, *MediaBuffer) { if chunkSize == 0 { // do not chunk return media, nil } mb := NewMediaBuffer(media, chunkSize) rdr, _, _, err := mb.Chunk() if err == io.EOF { // we can upload this in a single request return rdr, nil } // err might be a non-EOF error. If it is, the next call to mb.Chunk will // return the same error. Returning a MediaBuffer ensures that this error // will be handled at some point. return nil, mb }