github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/resource/api/upload.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package api
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"strconv"
    11  
    12  	"github.com/juju/errors"
    13  	charmresource "gopkg.in/juju/charm.v6-unstable/resource"
    14  	"gopkg.in/juju/names.v2"
    15  
    16  	"github.com/juju/juju/resource"
    17  )
    18  
    19  // UploadRequest defines a single upload request.
    20  type UploadRequest struct {
    21  	// Service is the application ID.
    22  	Service string
    23  
    24  	// Name is the resource name.
    25  	Name string
    26  
    27  	// Filename is the name of the file as it exists on disk.
    28  	Filename string
    29  
    30  	// Size is the size of the uploaded data, in bytes.
    31  	Size int64
    32  
    33  	// Fingerprint is the fingerprint of the uploaded data.
    34  	Fingerprint charmresource.Fingerprint
    35  
    36  	// PendingID is the pending ID to associate with this upload, if any.
    37  	PendingID string
    38  }
    39  
    40  // NewUploadRequest generates a new upload request for the given resource.
    41  func NewUploadRequest(service, name, filename string, r io.ReadSeeker) (UploadRequest, error) {
    42  	if !names.IsValidApplication(service) {
    43  		return UploadRequest{}, errors.Errorf("invalid application %q", service)
    44  	}
    45  
    46  	content, err := resource.GenerateContent(r)
    47  	if err != nil {
    48  		return UploadRequest{}, errors.Trace(err)
    49  	}
    50  
    51  	ur := UploadRequest{
    52  		Service:     service,
    53  		Name:        name,
    54  		Filename:    filename,
    55  		Size:        content.Size,
    56  		Fingerprint: content.Fingerprint,
    57  	}
    58  	return ur, nil
    59  }
    60  
    61  // ExtractUploadRequest pulls the required info from the HTTP request.
    62  func ExtractUploadRequest(req *http.Request) (UploadRequest, error) {
    63  	var ur UploadRequest
    64  
    65  	if req.Header.Get(HeaderContentLength) == "" {
    66  		req.Header.Set(HeaderContentLength, fmt.Sprint(req.ContentLength))
    67  	}
    68  
    69  	ctype := req.Header.Get(HeaderContentType)
    70  	if ctype != ContentTypeRaw {
    71  		return ur, errors.Errorf("unsupported content type %q", ctype)
    72  	}
    73  
    74  	service, name := ExtractEndpointDetails(req.URL)
    75  	fingerprint := req.Header.Get(HeaderContentSha384) // This parallels "Content-MD5".
    76  	sizeRaw := req.Header.Get(HeaderContentLength)
    77  	pendingID := req.URL.Query().Get(QueryParamPendingID)
    78  
    79  	fp, err := charmresource.ParseFingerprint(fingerprint)
    80  	if err != nil {
    81  		return ur, errors.Annotate(err, "invalid fingerprint")
    82  	}
    83  
    84  	filename, err := extractFilename(req)
    85  	if err != nil {
    86  		return ur, errors.Trace(err)
    87  	}
    88  
    89  	size, err := strconv.ParseInt(sizeRaw, 10, 64)
    90  	if err != nil {
    91  		return ur, errors.Annotate(err, "invalid size")
    92  	}
    93  
    94  	ur = UploadRequest{
    95  		Service:     service,
    96  		Name:        name,
    97  		Filename:    filename,
    98  		Size:        size,
    99  		Fingerprint: fp,
   100  		PendingID:   pendingID,
   101  	}
   102  	return ur, nil
   103  }
   104  
   105  func extractFilename(req *http.Request) (string, error) {
   106  	disp := req.Header.Get(HeaderContentDisposition)
   107  
   108  	// the first value returned here is the media type name (e.g. "form-data"),
   109  	// but we don't really care.
   110  	_, vals, err := parseMediaType(disp)
   111  	if err != nil {
   112  		return "", errors.Annotate(err, "badly formatted Content-Disposition")
   113  	}
   114  
   115  	param, ok := vals[filenameParamForContentDispositionHeader]
   116  	if !ok {
   117  		return "", errors.Errorf("missing filename in resource upload request")
   118  	}
   119  
   120  	filename, err := decodeParam(param)
   121  	if err != nil {
   122  		return "", errors.Annotatef(err, "couldn't decode filename %q from upload request", param)
   123  	}
   124  	return filename, nil
   125  }
   126  
   127  func setFilename(filename string, req *http.Request) {
   128  	filename = encodeParam(filename)
   129  
   130  	disp := formatMediaType(
   131  		MediaTypeFormData,
   132  		map[string]string{filenameParamForContentDispositionHeader: filename},
   133  	)
   134  
   135  	req.Header.Set(HeaderContentDisposition, disp)
   136  }
   137  
   138  // filenameParamForContentDispositionHeader is the name of the parameter that
   139  // contains the name of the file being uploaded, see mime.FormatMediaType and
   140  // RFC 1867 (http://tools.ietf.org/html/rfc1867):
   141  //
   142  //   The original local file name may be supplied as well, either as a
   143  //  'filename' parameter either of the 'content-disposition: form-data'
   144  //   header or in the case of multiple files in a 'content-disposition:
   145  //   file' header of the subpart.
   146  const filenameParamForContentDispositionHeader = "filename"
   147  
   148  // HTTPRequest generates a new HTTP request.
   149  func (ur UploadRequest) HTTPRequest() (*http.Request, error) {
   150  	// TODO(ericsnow) What about the rest of the URL?
   151  	urlStr := NewEndpointPath(ur.Service, ur.Name)
   152  
   153  	// TODO(natefinch): Use http.MethodPut when we upgrade to go1.5+.
   154  	req, err := http.NewRequest(MethodPut, urlStr, nil)
   155  	if err != nil {
   156  		return nil, errors.Trace(err)
   157  	}
   158  
   159  	req.Header.Set(HeaderContentType, ContentTypeRaw)
   160  	req.Header.Set(HeaderContentSha384, ur.Fingerprint.String())
   161  	req.Header.Set(HeaderContentLength, fmt.Sprint(ur.Size))
   162  	setFilename(ur.Filename, req)
   163  
   164  	req.ContentLength = ur.Size
   165  
   166  	if ur.PendingID != "" {
   167  		query := req.URL.Query()
   168  		query.Set(QueryParamPendingID, ur.PendingID)
   169  		req.URL.RawQuery = query.Encode()
   170  	}
   171  
   172  	return req, nil
   173  }
   174  
   175  type encoder interface {
   176  	Encode(charset, s string) string
   177  }
   178  
   179  type decoder interface {
   180  	Decode(s string) (string, error)
   181  }
   182  
   183  func encodeParam(s string) string {
   184  	return getEncoder().Encode("utf-8", s)
   185  }
   186  
   187  func decodeParam(s string) (string, error) {
   188  	decoded, err := getDecoder().Decode(s)
   189  
   190  	// If encoding is not required, the encoder will return the original string.
   191  	// However, the decoder doesn't expect that, so it barfs on non-encoded
   192  	// strings. To detect if a string was not encoded, we simply try encoding
   193  	// again, if it returns the same string, we know it wasn't encoded.
   194  	if err != nil && s == encodeParam(s) {
   195  		return s, nil
   196  	}
   197  	return decoded, err
   198  }