mirror of
https://github.com/httpie/cli.git
synced 2024-11-21 23:33:12 +01:00
Custom file upload MIME type (#927)
* Support curl-like syntax for custom MIME type for files In order to specify a custom MIME type for file uploads, a syntax similar to that used by cURL is used so that http -F test_file@/path/to/file.bin;type=application/zip https://... forwards the user-provided file type if provided, otherwise falling back to the usual guesswork out of the file extension.
This commit is contained in:
parent
492687b0da
commit
c4627cc882
@ -13,6 +13,7 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
|
||||
* Added ``--ciphers`` to allow configuring OpenSSL ciphers (`#870`_).
|
||||
* Added support for ``$XDG_CONFIG_HOME`` (`#920`_).
|
||||
* Fixed built-in plugins-related circular imports (`#925`_).
|
||||
* Fixed custom content types for each multipart uploaded file (`#668`_).
|
||||
|
||||
|
||||
`2.1.0`_ (2020-04-18)
|
||||
@ -441,3 +442,4 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
|
||||
.. _#895: https://github.com/jakubroztocil/httpie/issues/895
|
||||
.. _#920: https://github.com/jakubroztocil/httpie/issues/920
|
||||
.. _#925: https://github.com/jakubroztocil/httpie/issues/925
|
||||
.. _#668: https://github.com/jakubroztocil/httpie/issues/668
|
||||
|
@ -683,6 +683,13 @@ submitted:
|
||||
Note that ``@`` is used to simulate a file upload form field, whereas
|
||||
``=@`` just embeds the file content as a regular text field value.
|
||||
|
||||
When uploading files, their content type is inferred from the file name. You can manually
|
||||
override the inferred content type:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ http -f POST httpbin.org/post name='John Smith' cv@'~/files/data.bin;type=application/pdf'
|
||||
|
||||
|
||||
HTTP headers
|
||||
============
|
||||
|
@ -24,6 +24,7 @@ SEPARATOR_PROXY = ':'
|
||||
SEPARATOR_DATA_STRING = '='
|
||||
SEPARATOR_DATA_RAW_JSON = ':='
|
||||
SEPARATOR_FILE_UPLOAD = '@'
|
||||
SEPARATOR_FILE_UPLOAD_TYPE = ';type=' # in already parsed file upload path only
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS = '=@'
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE = ':=@'
|
||||
SEPARATOR_QUERY_PARAM = '=='
|
||||
|
@ -113,6 +113,7 @@ positional.add_argument(
|
||||
'@' Form file fields (only with --form, -f):
|
||||
|
||||
cs@~/Documents/CV.pdf
|
||||
cv@'~/Documents/CV.pdf;type=application/pdf'
|
||||
|
||||
'=@' A data field like '=', but takes a file path and embeds its content:
|
||||
|
||||
|
@ -6,7 +6,8 @@ from httpie.cli.argtypes import KeyValueArg
|
||||
from httpie.cli.constants import (
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS, SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||
SEPARATOR_DATA_RAW_JSON, SEPARATOR_DATA_STRING, SEPARATOR_FILE_UPLOAD,
|
||||
SEPARATOR_HEADER, SEPARATOR_HEADER_EMPTY, SEPARATOR_QUERY_PARAM,
|
||||
SEPARATOR_FILE_UPLOAD_TYPE, SEPARATOR_HEADER, SEPARATOR_HEADER_EMPTY,
|
||||
SEPARATOR_QUERY_PARAM,
|
||||
)
|
||||
from httpie.cli.dicts import (
|
||||
RequestDataDict, RequestFilesDict, RequestHeadersDict, RequestJSONDataDict,
|
||||
@ -95,7 +96,10 @@ def process_query_param_arg(arg: KeyValueArg) -> str:
|
||||
|
||||
|
||||
def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
|
||||
filename = arg.value
|
||||
parts = arg.value.split(SEPARATOR_FILE_UPLOAD_TYPE)
|
||||
filename = parts[0]
|
||||
mime_type = parts[1] if len(parts) > 1 else None
|
||||
|
||||
try:
|
||||
with open(os.path.expanduser(filename), 'rb') as f:
|
||||
contents = f.read()
|
||||
@ -104,7 +108,7 @@ def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
|
||||
return (
|
||||
os.path.basename(filename),
|
||||
BytesIO(contents),
|
||||
get_content_type(filename),
|
||||
mime_type or get_content_type(filename),
|
||||
)
|
||||
|
||||
|
||||
|
@ -17,27 +17,37 @@ class TestMultipartFormDataFileUpload:
|
||||
|
||||
def test_upload_ok(self, httpbin):
|
||||
r = http('--form', '--verbose', 'POST', httpbin.url + '/post',
|
||||
'test-file@%s' % FILE_PATH_ARG, 'foo=bar')
|
||||
f'test-file@{FILE_PATH_ARG}', 'foo=bar')
|
||||
assert HTTP_OK in r
|
||||
assert 'Content-Disposition: form-data; name="foo"' in r
|
||||
assert 'Content-Disposition: form-data; name="test-file";' \
|
||||
' filename="%s"' % os.path.basename(FILE_PATH) in r
|
||||
f' filename="{os.path.basename(FILE_PATH)}"' in r
|
||||
assert FILE_CONTENT in r
|
||||
assert '"foo": "bar"' in r
|
||||
assert 'Content-Type: text/plain' in r
|
||||
|
||||
def test_upload_multiple_fields_with_the_same_name(self, httpbin):
|
||||
r = http('--form', '--verbose', 'POST', httpbin.url + '/post',
|
||||
'test-file@%s' % FILE_PATH_ARG,
|
||||
'test-file@%s' % FILE_PATH_ARG)
|
||||
f'test-file@{FILE_PATH_ARG}',
|
||||
f'test-file@{FILE_PATH_ARG}')
|
||||
assert HTTP_OK in r
|
||||
assert r.count('Content-Disposition: form-data; name="test-file";'
|
||||
' filename="%s"' % os.path.basename(FILE_PATH)) == 2
|
||||
f' filename="{os.path.basename(FILE_PATH)}"') == 2
|
||||
# Should be 4, but is 3 because httpbin
|
||||
# doesn't seem to support filed field lists
|
||||
assert r.count(FILE_CONTENT) in [3, 4]
|
||||
assert r.count('Content-Type: text/plain') == 2
|
||||
|
||||
def test_upload_custom_content_type(self, httpbin):
|
||||
r = http('--form', '--verbose', 'POST', httpbin.url + '/post',
|
||||
f'test-file@{FILE_PATH_ARG};type=image/vnd.microsoft.icon')
|
||||
assert HTTP_OK in r
|
||||
# Content type is stripped from the filename
|
||||
assert 'Content-Disposition: form-data; name="test-file";' \
|
||||
f' filename="{os.path.basename(FILE_PATH)}"' in r
|
||||
assert FILE_CONTENT in r
|
||||
assert 'Content-Type: image/vnd.microsoft.icon' in r
|
||||
|
||||
|
||||
class TestRequestBodyFromFilePath:
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user