Skip to content

Commit

Permalink
feature: file upload allows buffer and streams (#389)
Browse files Browse the repository at this point in the history
  • Loading branch information
Amin Mahboubi committed Oct 1, 2020
1 parent 3fa5aa2 commit d8a5efb
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 17 deletions.
2 changes: 1 addition & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ export class StreamClient<

upload(
url: string,
uri: string | File | NodeJS.ReadStream,
uri: string | File | Buffer | NodeJS.ReadStream,
name?: string,
contentType?: string,
onUploadProgress?: OnUploadProgress,
Expand Down
4 changes: 2 additions & 2 deletions src/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ export class StreamFileStore {
// param. If you don't then Android will refuse to perform the upload
/**
* upload a File instance or a readable stream of data
* @param {File|NodeJS.ReadStream|string} uri - File object or stream or URI
* @param {File|Buffer|NodeJS.ReadStream|string} uri - File object or stream or URI
* @param {string} [name] - file name
* @param {string} [contentType] - mime-type
* @param {function} [onUploadProgress] - browser only, Function that is called with upload progress
* @return {Promise<FileUploadAPIResponse>}
*/
upload(
uri: string | File | NodeJS.ReadStream,
uri: string | File | Buffer | NodeJS.ReadStream,
name?: string,
contentType?: string,
onUploadProgress?: OnUploadProgress,
Expand Down
4 changes: 2 additions & 2 deletions src/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ export class StreamImageStore {
// param. If you don't then Android will refuse to perform the upload
/**
* upload an Image File instance or a readable stream of data
* @param {File|NodeJS.ReadStream|string} uri - File object or stream or URI
* @param {File|Buffer|NodeJS.ReadStream|string} uri - File object or stream or URI
* @param {string} [name] - file name
* @param {string} [contentType] - mime-type
* @param {function} [onUploadProgress] - browser only, Function that is called with upload progress
* @return {Promise<FileUploadAPIResponse>}
*/
upload(
uri: string | File | NodeJS.ReadStream,
uri: string | File | Buffer | NodeJS.ReadStream,
name?: string,
contentType?: string,
onUploadProgress?: OnUploadProgress,
Expand Down
45 changes: 33 additions & 12 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import FormData from 'form-data';
import FormData, { AppendOptions } from 'form-data';

import { FeedError } from './errors';

Expand Down Expand Up @@ -31,8 +31,27 @@ function rfc3986(str: string) {
return str.replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
}

function isReadableStream(obj: NodeJS.ReadStream): obj is NodeJS.ReadStream {
return obj !== null && typeof obj === 'object' && typeof (obj as NodeJS.ReadStream)._read === 'function';
function isReadableStream(obj: unknown): obj is NodeJS.ReadStream {
return (
obj !== null &&
typeof obj === 'object' &&
((obj as NodeJS.ReadStream).readable || typeof (obj as NodeJS.ReadStream)._read === 'function')
);
}

function isBuffer(obj: unknown): obj is Buffer {
return (
obj != null &&
(obj as Buffer).constructor != null &&
// @ts-expect-error
typeof obj.constructor.isBuffer === 'function' &&
// @ts-expect-error
obj.constructor.isBuffer(obj)
);
}

function isFileWebAPI(uri: unknown): uri is File {
return typeof window !== 'undefined' && 'File' in window && uri instanceof File;
}

/*
Expand All @@ -50,21 +69,23 @@ function validateFeedId(feedId: string) {
return feedId;
}

function addFileToFormData(uri: string | File | NodeJS.ReadStream, name?: string, contentType?: string) {
function addFileToFormData(uri: string | File | Buffer | NodeJS.ReadStream, name?: string, contentType?: string) {
const data = new FormData();

let fileField: File | NodeJS.ReadStream | { name: string; uri: string; type?: string };
const appendOptions: AppendOptions = {};
if (name) appendOptions.filename = name;
if (contentType) appendOptions.contentType = contentType;

if (isReadableStream(uri as NodeJS.ReadStream)) {
fileField = uri as NodeJS.ReadStream;
} else if (uri && uri.toString && uri.toString() === '[object File]') {
fileField = uri as File;
if (isReadableStream(uri) || isBuffer(uri) || isFileWebAPI(uri)) {
data.append('file', uri, appendOptions);
} else {
fileField = { uri: uri as string, name: name || (uri as string).split('/').reverse()[0] };
if (contentType != null) fileField.type = contentType;
data.append('file', {
uri,
name: name || (uri as string).split('/').reverse()[0],
contentType: contentType || undefined,
});
}

data.append('file', fileField);
return data;
}

Expand Down
16 changes: 16 additions & 0 deletions test/integration/cloud/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ describe('Files', () => {
});
});

describe('When alice adds a different type of file stream', () => {
ctx.requestShouldNotError(async () => {
const file = request('http://nodejs.org/images/logo.png');
ctx.response = await ctx.alice.files.upload(file);
ctx.response.should.not.be.empty;
});
});

describe('When alice adds a buffer as a file', () => {
ctx.requestShouldNotError(async () => {
const file = Buffer.from('some string', 'binary');
ctx.response = await ctx.alice.files.upload(file, 'x.txt', 'text/plain');
ctx.response.should.not.be.empty;
});
});

describe('When alice adds a big file', () => {
ctx.requestShouldError(413, async () => {
const file = fs.createReadStream('./test/integration/cloud/toolarge2.jpeg');
Expand Down

0 comments on commit d8a5efb

Please sign in to comment.