mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-20 22:27:24 -04:00
Added file upload support (#48)
This commit is contained in:
@@ -116,22 +116,19 @@ export const CONTENT_TYPE_XML = 'application/xml';
|
||||
export const CONTENT_TYPE_TEXT = 'text/plain';
|
||||
export const CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded';
|
||||
export const CONTENT_TYPE_FORM_DATA = 'multipart/form-data';
|
||||
export const CONTENT_TYPE_OTHER = '';
|
||||
export const CONTENT_TYPE_FILE = 'application/octet-stream';
|
||||
export const CONTENT_TYPE_RAW = '';
|
||||
|
||||
export const contentTypesMap = {
|
||||
[CONTENT_TYPE_JSON]: 'JSON',
|
||||
[CONTENT_TYPE_XML]: 'XML',
|
||||
// [CONTENT_TYPE_FORM_DATA]: 'Form Data',
|
||||
[CONTENT_TYPE_FORM_URLENCODED]: 'Url Encoded',
|
||||
[CONTENT_TYPE_FORM_URLENCODED]: 'Form Url Encoded',
|
||||
[CONTENT_TYPE_TEXT]: 'Plain Text',
|
||||
[CONTENT_TYPE_OTHER]: 'Other',
|
||||
[CONTENT_TYPE_FILE]: 'File Upload',
|
||||
[CONTENT_TYPE_RAW]: 'Raw Body',
|
||||
};
|
||||
|
||||
export const BODY_TYPE_RAW = 'raw';
|
||||
export const BODY_TYPE_FILE = 'file';
|
||||
export const BODY_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded';
|
||||
export const BODY_TYPE_FORM = 'multipart/form-data';
|
||||
|
||||
/**
|
||||
* Get the friendly name for a given content type
|
||||
*
|
||||
@@ -139,7 +136,7 @@ export const BODY_TYPE_FORM = 'multipart/form-data';
|
||||
* @returns {*|string}
|
||||
*/
|
||||
export function getContentTypeName (contentType) {
|
||||
return contentTypesMap[contentType] || contentTypesMap[CONTENT_TYPE_OTHER];
|
||||
return contentTypesMap[contentType] || 'Other';
|
||||
}
|
||||
|
||||
export function getContentTypeFromHeaders (headers) {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import fs from 'fs';
|
||||
import * as models from '../models';
|
||||
import {getRenderedRequest} from './render';
|
||||
import {jarFromCookies} from './cookies';
|
||||
import * as util from './misc';
|
||||
import * as misc from './misc';
|
||||
import {CONTENT_TYPE_FILE} from './constants';
|
||||
import {newBodyRaw} from '../models/request';
|
||||
|
||||
export function exportHarWithRequest (renderedRequest, addContentLength = false) {
|
||||
if (addContentLength) {
|
||||
@@ -18,8 +21,17 @@ export function exportHarWithRequest (renderedRequest, addContentLength = false)
|
||||
}
|
||||
}
|
||||
|
||||
// Luckily, Insomnia uses the same body format as HAR :)
|
||||
const postData = renderedRequest.body;
|
||||
let postData = '';
|
||||
if (renderedRequest.body.fileName) {
|
||||
try {
|
||||
postData = newBodyRaw(fs.readFileSync(renderedRequest.body.fileName, 'base64'));
|
||||
} catch (e) {
|
||||
console.warn('[code gen] Failed to read file', e);
|
||||
}
|
||||
} else {
|
||||
// For every other type, Insomnia uses the same body format as HAR
|
||||
postData = renderedRequest.body;
|
||||
}
|
||||
|
||||
return {
|
||||
method: renderedRequest.method,
|
||||
|
||||
@@ -154,3 +154,25 @@ export function debounce (callback, millis = DEBOUNCE_MILLIS) {
|
||||
callback.apply(null, results['__key__'])
|
||||
}, millis).bind(null, '__key__');
|
||||
}
|
||||
|
||||
export function describeByteSize (bytes) {
|
||||
bytes = Math.round(bytes * 10) / 10;
|
||||
let size;
|
||||
|
||||
let unit = 'B';
|
||||
if (bytes < 1024) {
|
||||
size = bytes;
|
||||
unit = 'B';
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
size = bytes / 1024;
|
||||
unit = 'KB';
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
size = bytes / 1024 / 1024;
|
||||
unit = 'MB';
|
||||
} else {
|
||||
size = bytes / 1024 / 1024 / 1024;
|
||||
unit = 'GB';
|
||||
}
|
||||
|
||||
return Math.round(size * 10) / 10 + ' ' + unit;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import {jarFromCookies, cookiesFromJar, cookieHeaderValueForUri} from './cookies
|
||||
import {setDefaultProtocol} from './misc';
|
||||
import {getRenderedRequest} from './render';
|
||||
import {swapHost} from './dns';
|
||||
import {CONTENT_TYPE_FILE} from './constants';
|
||||
import * as fs from 'fs';
|
||||
|
||||
let cancelRequestFunction = null;
|
||||
|
||||
@@ -50,6 +52,8 @@ export function _buildRequestConfig (renderedRequest, patch = {}) {
|
||||
config.body = buildFromParams(renderedRequest.body.params || [], true);
|
||||
} else if (renderedRequest.body.mimeType === CONTENT_TYPE_FORM_DATA) {
|
||||
// TODO: This
|
||||
} else if (renderedRequest.body.mimeType === CONTENT_TYPE_FILE) {
|
||||
config.body = fs.readFileSync(renderedRequest.body.fileName);
|
||||
} else {
|
||||
config.body = renderedRequest.body.text || '';
|
||||
}
|
||||
@@ -85,14 +89,25 @@ export function _actuallySend (renderedRequest, settings, forceIPv4 = false) {
|
||||
const proxyHost = protocol === 'https:' ? httpsProxy : httpProxy;
|
||||
const proxy = proxyHost ? setDefaultProtocol(proxyHost) : null;
|
||||
|
||||
const config = _buildRequestConfig(renderedRequest, {
|
||||
jar: null, // We're doing our own cookies
|
||||
proxy: proxy,
|
||||
followAllRedirects: settings.followRedirects,
|
||||
followRedirect: settings.followRedirects,
|
||||
timeout: settings.timeout > 0 ? settings.timeout : null,
|
||||
rejectUnauthorized: settings.validateSSL
|
||||
}, true);
|
||||
let config;
|
||||
try {
|
||||
config = _buildRequestConfig(renderedRequest, {
|
||||
jar: null, // We're doing our own cookies
|
||||
proxy: proxy,
|
||||
followAllRedirects: settings.followRedirects,
|
||||
followRedirect: settings.followRedirects,
|
||||
timeout: settings.timeout > 0 ? settings.timeout : null,
|
||||
rejectUnauthorized: settings.validateSSL
|
||||
}, true);
|
||||
} catch (e) {
|
||||
const response = await models.response.create({
|
||||
parentId: renderedRequest._id,
|
||||
elapsedTime: 0,
|
||||
statusMessage: 'Error',
|
||||
error: e.message
|
||||
});
|
||||
return resolve(response);
|
||||
}
|
||||
|
||||
// Add the cookie header to the request
|
||||
const cookieJar = renderedRequest.cookieJar;
|
||||
|
||||
@@ -3,6 +3,9 @@ import * as db from '../common/database';
|
||||
import {getContentTypeHeader} from '../common/misc';
|
||||
import {deconstructToParams} from '../common/querystring';
|
||||
import {CONTENT_TYPE_JSON} from '../common/constants';
|
||||
import {CONTENT_TYPE_XML} from '../common/constants';
|
||||
import {CONTENT_TYPE_FILE} from '../common/constants';
|
||||
import {CONTENT_TYPE_TEXT} from '../common/constants';
|
||||
|
||||
export const name = 'Request';
|
||||
export const type = 'Request';
|
||||
@@ -22,6 +25,24 @@ export function init () {
|
||||
};
|
||||
}
|
||||
|
||||
export function getBodyDescription (body) {
|
||||
if (body.fileName) {
|
||||
return 'File Upload';
|
||||
} else if (body.mimeType === CONTENT_TYPE_FORM_URLENCODED) {
|
||||
return 'Form Url Encoded';
|
||||
} else if (body.mimeType === CONTENT_TYPE_FORM_DATA) {
|
||||
return 'Form Data';
|
||||
} else if (body.mimeType === CONTENT_TYPE_JSON) {
|
||||
return 'JSON';
|
||||
} else if (body.mimeType === CONTENT_TYPE_XML) {
|
||||
return 'XML';
|
||||
} else if (body.mimeType === CONTENT_TYPE_TEXT) {
|
||||
return 'Plain Text';
|
||||
} else {
|
||||
return 'Raw Body';
|
||||
}
|
||||
}
|
||||
|
||||
export function newBodyRaw (rawBody, contentType) {
|
||||
if (!contentType) {
|
||||
return {text: rawBody};
|
||||
@@ -38,6 +59,13 @@ export function newBodyFormUrlEncoded (parameters) {
|
||||
}
|
||||
}
|
||||
|
||||
export function newBodyFile (path) {
|
||||
return {
|
||||
mimeType: CONTENT_TYPE_FILE,
|
||||
fileName: path
|
||||
}
|
||||
}
|
||||
|
||||
export function newBodyForm (parameters) {
|
||||
return {
|
||||
mimeType: CONTENT_TYPE_FORM_DATA,
|
||||
@@ -99,6 +127,8 @@ export function updateMimeType (request, mimeType) {
|
||||
request.body = newBodyForm(request.body.params || []);
|
||||
} else if (mimeType === CONTENT_TYPE_JSON) {
|
||||
request.body = newBodyRaw(request.body.text || '');
|
||||
} else if (mimeType === CONTENT_TYPE_FILE) {
|
||||
request.body = newBodyFile('');
|
||||
} else {
|
||||
request.body = newBodyRaw(request.body.text || '', mimeType);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import AuthEditor from './editors/AuthEditor';
|
||||
import RequestUrlBar from './RequestUrlBar.js';
|
||||
import {MOD_SYM, getContentTypeName, getContentTypeFromHeaders} from '../../common/constants';
|
||||
import {debounce} from '../../common/misc';
|
||||
import {getBodyDescription} from '../../models/request';
|
||||
|
||||
class RequestPane extends Component {
|
||||
render () {
|
||||
|
||||
55
app/ui/components/base/FileInputButton.js
Normal file
55
app/ui/components/base/FileInputButton.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {remote} from 'electron';
|
||||
import {Dropdown, DropdownButton, DropdownItem} from '../base/dropdown';
|
||||
import PromptButton from '../base/PromptButton';
|
||||
|
||||
class FileInputButton extends Component {
|
||||
_handleUnsetFile () {
|
||||
this.props.onChange('');
|
||||
}
|
||||
|
||||
_handleChooseFile () {
|
||||
const options = {
|
||||
title: 'Import File',
|
||||
buttonLabel: 'Import',
|
||||
properties: ['openFile']
|
||||
};
|
||||
|
||||
remote.dialog.showOpenDialog(options, async paths => {
|
||||
if (!paths || paths.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = paths[0];
|
||||
this.props.onChange(path);
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const {className} = this.props;
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownButton className={className}>
|
||||
Choose File <i className="fa fa-caret-down"></i>
|
||||
</DropdownButton>
|
||||
<DropdownItem onClick={e => this._handleChooseFile()}>
|
||||
<i className="fa fa-file"></i>
|
||||
Choose File
|
||||
</DropdownItem>
|
||||
<DropdownItem buttonClass={PromptButton}
|
||||
addIcon={true}
|
||||
onClick={e => this._handleUnsetFile()}>
|
||||
<i className="fa fa-close"></i>
|
||||
Unset
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FileInputButton.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default FileInputButton;
|
||||
@@ -2,10 +2,11 @@ import React, {PropTypes, Component} from 'react';
|
||||
import RawEditor from './RawEditor';
|
||||
import UrlEncodedEditor from './UrlEncodedEditor';
|
||||
import FormEditor from './FormEditor';
|
||||
import {getContentTypeFromHeaders, BODY_TYPE_FORM_URLENCODED, BODY_TYPE_FORM, BODY_TYPE_FILE} from '../../../../common/constants';
|
||||
import FileEditor from './FileEditor';
|
||||
import {getContentTypeFromHeaders, CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_FORM_DATA} from '../../../../common/constants';
|
||||
import {newBodyRaw, newBodyFormUrlEncoded, newBodyForm} from '../../../../models/request';
|
||||
import {CONTENT_TYPE_FORM_URLENCODED} from '../../../../common/constants';
|
||||
import {CONTENT_TYPE_FORM_DATA} from '../../../../common/constants';
|
||||
import {CONTENT_TYPE_FILE} from '../../../../common/constants';
|
||||
import {newBodyFile} from '../../../../models/request';
|
||||
|
||||
class BodyEditor extends Component {
|
||||
constructor (props) {
|
||||
@@ -13,6 +14,7 @@ class BodyEditor extends Component {
|
||||
this._boundHandleRawChange = this._handleRawChange.bind(this);
|
||||
this._boundHandleFormUrlEncodedChange = this._handleFormUrlEncodedChange.bind(this);
|
||||
this._boundHandleFormChange = this._handleFormChange.bind(this);
|
||||
this._boundHandleFileChange = this._handleFileChange.bind(this);
|
||||
}
|
||||
|
||||
_handleRawChange (rawValue) {
|
||||
@@ -36,6 +38,12 @@ class BodyEditor extends Component {
|
||||
onChange(newBody);
|
||||
}
|
||||
|
||||
_handleFileChange (path) {
|
||||
const {onChange} = this.props;
|
||||
const newBody = newBodyFile(path);
|
||||
onChange(newBody);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {fontSize, lineWrapping, request} = this.props;
|
||||
const bodyType = request.body.mimeType;
|
||||
@@ -57,9 +65,14 @@ class BodyEditor extends Component {
|
||||
parameters={request.body.params || []}
|
||||
/>
|
||||
)
|
||||
} else if (bodyType === BODY_TYPE_FILE) {
|
||||
// TODO
|
||||
return null
|
||||
} else if (bodyType === CONTENT_TYPE_FILE) {
|
||||
return (
|
||||
<FileEditor
|
||||
key={request._id}
|
||||
onChange={this._boundHandleFileChange}
|
||||
path={fileName || ''}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
const contentType = getContentTypeFromHeaders(request.headers);
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import fs from 'fs';
|
||||
import electron from 'electron';
|
||||
import React, {PropTypes, Component} from 'react';
|
||||
import FileInputButton from '../../base/FileInputButton';
|
||||
import * as misc from '../../../../common/misc';
|
||||
|
||||
class FileEditor extends Component {
|
||||
render () {
|
||||
const {path, onChange} = this.props;
|
||||
|
||||
// Replace home path with ~/ to make the path shorter
|
||||
const homeDirectory = electron.remote.app.getPath('home');
|
||||
const pathDescription = path.replace(homeDirectory, '~');
|
||||
|
||||
let sizeDescription = '';
|
||||
try {
|
||||
const bytes = fs.statSync(path).size;
|
||||
sizeDescription = misc.describeByteSize(bytes);
|
||||
} catch (e) {
|
||||
sizeDescription = '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-center pad">
|
||||
<p className="txt-sm pad-top">
|
||||
{path ? (
|
||||
<code className="wrap">
|
||||
<span className="force-wrap selectable" title={path}>
|
||||
{pathDescription}
|
||||
</span>
|
||||
{" "}
|
||||
<span className="no-wrap italic">({sizeDescription})</span>
|
||||
</code>
|
||||
) : (
|
||||
<code className="super-faint">No file selected</code>
|
||||
)}
|
||||
</p>
|
||||
<FileInputButton
|
||||
path={path}
|
||||
className="btn btn--super-compact btn--outlined"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FileEditor.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default FileEditor;
|
||||
|
||||
@@ -1,25 +1,8 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import * as misc from '../../../common/misc';
|
||||
|
||||
const SizeTag = props => {
|
||||
const bytes = Math.round(props.bytes * 10) / 10;
|
||||
let size;
|
||||
|
||||
let unit = 'B';
|
||||
if (bytes < 1024) {
|
||||
size = bytes;
|
||||
unit = 'B';
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
size = bytes / 1024;
|
||||
unit = 'KB';
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
size = bytes / 1024 / 1024;
|
||||
unit = 'MB';
|
||||
} else {
|
||||
size = bytes / 1024 / 1024 / 1024;
|
||||
unit = 'GB';
|
||||
}
|
||||
|
||||
const responseSizeString = Math.round(size * 10) / 10 + ' ' + unit;
|
||||
const responseSizeString = misc.describeByteSize(props.bytes);
|
||||
|
||||
return (
|
||||
<div className="tag">
|
||||
|
||||
@@ -87,20 +87,30 @@ label > .form-control,
|
||||
}
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
.btn:disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
button:focus:not(:disabled),
|
||||
button.focus:not(:disabled),
|
||||
button:hover:not(:disabled) {
|
||||
.btn:focus:not(:disabled),
|
||||
.btn.focus:not(:disabled),
|
||||
.btn:hover:not(:disabled) {
|
||||
background: @hl-xs;
|
||||
}
|
||||
|
||||
button:active:not(:disabled) {
|
||||
.btn:active:not(:disabled) {
|
||||
background: @hl-md;
|
||||
}
|
||||
|
||||
.btn.btn--no-background {
|
||||
opacity: 0.5;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
textarea, input, button {
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
|
||||
Reference in New Issue
Block a user