Added file upload support (#48)

This commit is contained in:
Gregory Schier
2016-11-22 14:26:52 -08:00
committed by GitHub
parent b14eabbdc9
commit d6e5f2f873
11 changed files with 240 additions and 49 deletions

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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 () {

View 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;

View File

@@ -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 (

View File

@@ -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;

View File

@@ -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">

View File

@@ -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;